OPENDJ-1602 (CR-5566) New pluggable storage based backend
Copied the JE backend (without import) under the pluggable backend package.
Changed it to use the proof-of-concept backend interfaces instead of JE code.
BackendImpl.java:
Added interfaces from the backend proof-of-concept.
*.java:
Stripped all JE imports and replaced them with the backend proof-of-concept interfaces.
build.xml:
Code does not compile, so excluded "org.opends.server.backends.pluggable" from checkstyle checks and from compilation process.
StaticUtils.java:
Added bytesToHex(ByteSequence).
42 files added
2 files modified
| | |
| | | |
| | | <!-- General server-wide properties --> |
| | | <property name="src.dir" location="src/server" /> |
| | | <property name="pluggablebackend.pkg" value="org/opends/server/backends/pluggable" /> |
| | | <property name="build.dir" location="build" /> |
| | | <property name="classes.dir" location="${build.dir}/classes" /> |
| | | <property name="build.lib.dir" location="${build.dir}/lib" /> |
| | |
| | | <fileset dir="${src.dir}"> |
| | | <include name="**/*.java"/> |
| | | <exclude name="**/PublicAPI.java" /> |
| | | <exclude name="${pluggablebackend.pkg}/*.java" /> |
| | | </fileset> |
| | | <formatter type="plain" /> |
| | | </checkstyle> |
| | |
| | | <mkdir dir="${build.lib.dir}" /> |
| | | |
| | | <javac srcdir="${src.dir}:${admin.src.dir}:${msg.src.dir}:${msg.javagen.dir}:${ads.src.dir}:${quicksetup.src.dir}:${guitools.src.dir}" |
| | | excludes="${pluggablebackend.pkg}/**" |
| | | destdir="${classes.dir}"> |
| | | <classpath> |
| | | <fileset refid="opendj.runtime.jars"/> |
| New file |
| | |
| | | /* |
| | | * CDDL HEADER START |
| | | * |
| | | * The contents of this file are subject to the terms of the |
| | | * Common Development and Distribution License, Version 1.0 only |
| | | * (the "License"). You may not use this file except in compliance |
| | | * with the License. |
| | | * |
| | | * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt |
| | | * or http://forgerock.org/license/CDDLv1.0.html. |
| | | * See the License for the specific language governing permissions |
| | | * and limitations under the License. |
| | | * |
| | | * When distributing Covered Code, include this CDDL HEADER in each |
| | | * file and include the License file at legal-notices/CDDLv1_0.txt. |
| | | * If applicable, add the following below this CDDL HEADER, with the |
| | | * fields enclosed by brackets "[]" replaced with your own identifying |
| | | * information: |
| | | * Portions Copyright [yyyy] [name of copyright owner] |
| | | * |
| | | * CDDL HEADER END |
| | | * |
| | | * |
| | | * Copyright 2006-2010 Sun Microsystems, Inc. |
| | | * Portions Copyright 2011-2014 ForgeRock AS |
| | | * Portions Copyright 2014 Manuel Gaupp |
| | | */ |
| | | package org.opends.server.backends.pluggable; |
| | | |
| | | import java.io.Closeable; |
| | | import java.util.*; |
| | | import java.util.concurrent.atomic.AtomicBoolean; |
| | | |
| | | import org.forgerock.i18n.LocalizableMessage; |
| | | import org.forgerock.i18n.LocalizableMessageBuilder; |
| | | import org.forgerock.i18n.slf4j.LocalizedLogger; |
| | | import org.forgerock.opendj.config.server.ConfigException; |
| | | import org.forgerock.opendj.ldap.Assertion; |
| | | import org.forgerock.opendj.ldap.ByteString; |
| | | import org.forgerock.opendj.ldap.DecodeException; |
| | | import org.forgerock.opendj.ldap.ResultCode; |
| | | import org.forgerock.opendj.ldap.schema.MatchingRule; |
| | | import org.forgerock.opendj.ldap.spi.IndexQueryFactory; |
| | | import org.forgerock.opendj.ldap.spi.IndexingOptions; |
| | | import org.forgerock.util.Utils; |
| | | import org.opends.server.admin.server.ConfigurationChangeListener; |
| | | import org.opends.server.admin.std.meta.LocalDBIndexCfgDefn.IndexType; |
| | | import org.opends.server.admin.std.server.LocalDBIndexCfg; |
| | | import org.opends.server.backends.pluggable.BackendImpl.StorageRuntimeException; |
| | | import org.opends.server.backends.pluggable.BackendImpl.TreeName; |
| | | import org.opends.server.core.DirectoryServer; |
| | | import org.opends.server.monitors.DatabaseEnvironmentMonitor; |
| | | import org.opends.server.types.*; |
| | | import org.opends.server.util.StaticUtils; |
| | | |
| | | import static org.opends.messages.JebMessages.*; |
| | | import static org.opends.server.util.ServerConstants.*; |
| | | import static org.opends.server.util.StaticUtils.*; |
| | | |
| | | /** |
| | | * Class representing an attribute index. |
| | | * We have a separate database for each type of indexing, which makes it easy |
| | | * to tell which attribute indexes are configured. The different types of |
| | | * indexing are equality, presence, substrings and ordering. The keys in the |
| | | * ordering index are ordered by setting the btree comparator to the ordering |
| | | * matching rule comparator. |
| | | * Note that the values in the equality index are normalized by the equality |
| | | * matching rule, whereas the values in the ordering index are normalized |
| | | * by the ordering matching rule. If these could be guaranteed to be identical |
| | | * then we would not need a separate ordering index. |
| | | */ |
| | | public class AttributeIndex |
| | | implements ConfigurationChangeListener<LocalDBIndexCfg>, Closeable |
| | | { |
| | | private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); |
| | | |
| | | /** Type of the index filter. */ |
| | | static enum IndexFilterType |
| | | { |
| | | /** Equality. */ |
| | | EQUALITY(IndexType.EQUALITY), |
| | | /** Presence. */ |
| | | PRESENCE(IndexType.PRESENCE), |
| | | /** Ordering. */ |
| | | GREATER_OR_EQUAL(IndexType.ORDERING), |
| | | /** Ordering. */ |
| | | LESS_OR_EQUAL(IndexType.ORDERING), |
| | | /** Substring. */ |
| | | SUBSTRING(IndexType.SUBSTRING), |
| | | /** Approximate. */ |
| | | APPROXIMATE(IndexType.APPROXIMATE); |
| | | |
| | | private final IndexType indexType; |
| | | |
| | | private IndexFilterType(IndexType indexType) |
| | | { |
| | | this.indexType = indexType; |
| | | } |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public String toString() |
| | | { |
| | | return indexType.toString(); |
| | | } |
| | | } |
| | | |
| | | /* |
| | | * FIXME Matthew Swift: Once the matching rules have been migrated we should |
| | | * revisit this class. All of the evaluateXXX methods should go (the Matcher |
| | | * class in the SDK could implement the logic, I hope). |
| | | */ |
| | | |
| | | /** The entryContainer in which this attribute index resides. */ |
| | | private final EntryContainer entryContainer; |
| | | |
| | | /** The attribute index configuration. */ |
| | | private LocalDBIndexCfg indexConfig; |
| | | |
| | | /** The mapping from names to indexes. */ |
| | | private final Map<String, Index> nameToIndexes = new HashMap<String, Index>(); |
| | | private final IndexQueryFactory<IndexQuery> indexQueryFactory; |
| | | |
| | | /** |
| | | * The mapping from extensible index types (e.g. "substring" or "shared") to list of indexes. |
| | | */ |
| | | private Map<String, Collection<Index>> extensibleIndexesMapping; |
| | | |
| | | /** |
| | | * Create a new attribute index object. |
| | | * |
| | | * @param indexConfig The attribute index configuration. |
| | | * @param entryContainer The entryContainer of this attribute index. |
| | | * @throws ConfigException if a configuration related error occurs. |
| | | */ |
| | | public AttributeIndex(LocalDBIndexCfg indexConfig, EntryContainer entryContainer) throws ConfigException |
| | | { |
| | | this.entryContainer = entryContainer; |
| | | this.indexConfig = indexConfig; |
| | | |
| | | buildPresenceIndex(); |
| | | buildIndexes(IndexType.EQUALITY); |
| | | buildIndexes(IndexType.SUBSTRING); |
| | | buildIndexes(IndexType.ORDERING); |
| | | buildIndexes(IndexType.APPROXIMATE); |
| | | buildExtensibleIndexes(); |
| | | |
| | | final JEIndexConfig config = new JEIndexConfig(indexConfig.getSubstringLength()); |
| | | indexQueryFactory = new IndexQueryFactoryImpl(nameToIndexes, config); |
| | | extensibleIndexesMapping = computeExtensibleIndexesMapping(); |
| | | } |
| | | |
| | | private void buildPresenceIndex() |
| | | { |
| | | final IndexType indexType = IndexType.PRESENCE; |
| | | if (indexConfig.getIndexType().contains(indexType)) |
| | | { |
| | | String indexID = indexType.toString(); |
| | | nameToIndexes.put(indexID, newPresenceIndex(indexConfig)); |
| | | } |
| | | } |
| | | |
| | | private Index newPresenceIndex(LocalDBIndexCfg cfg) |
| | | { |
| | | final AttributeType attrType = cfg.getAttribute(); |
| | | final TreeName indexName = getIndexName(attrType, IndexType.PRESENCE.toString()); |
| | | final PresenceIndexer indexer = new PresenceIndexer(attrType); |
| | | return entryContainer.newIndexForAttribute(indexName, indexer, cfg.getIndexEntryLimit()); |
| | | } |
| | | |
| | | private void buildExtensibleIndexes() throws ConfigException |
| | | { |
| | | final IndexType indexType = IndexType.EXTENSIBLE; |
| | | if (indexConfig.getIndexType().contains(indexType)) |
| | | { |
| | | final AttributeType attrType = indexConfig.getAttribute(); |
| | | Set<String> extensibleRules = indexConfig.getIndexExtensibleMatchingRule(); |
| | | if (extensibleRules == null || extensibleRules.isEmpty()) |
| | | { |
| | | throw new ConfigException(ERR_CONFIG_INDEX_TYPE_NEEDS_MATCHING_RULE.get(attrType, indexType.toString())); |
| | | } |
| | | |
| | | // Iterate through the Set and create the index only if necessary. |
| | | // Collation equality and Ordering matching rules share the same indexer and index |
| | | // A Collation substring matching rule is treated differently |
| | | // as it uses a separate indexer and index. |
| | | for (final String ruleName : extensibleRules) |
| | | { |
| | | MatchingRule rule = DirectoryServer.getMatchingRule(toLowerCase(ruleName)); |
| | | if (rule == null) |
| | | { |
| | | logger.error(ERR_CONFIG_INDEX_TYPE_NEEDS_VALID_MATCHING_RULE, attrType, ruleName); |
| | | continue; |
| | | } |
| | | for (org.forgerock.opendj.ldap.spi.Indexer indexer : rule.getIndexers()) |
| | | { |
| | | final String indexId = indexer.getIndexID(); |
| | | if (!nameToIndexes.containsKey(indexId)) |
| | | { |
| | | // There is no index available for this index id. Create a new index |
| | | nameToIndexes.put(indexId, newAttributeIndex(indexConfig, indexer)); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | private void buildIndexes(IndexType indexType) throws ConfigException |
| | | { |
| | | if (indexConfig.getIndexType().contains(indexType)) |
| | | { |
| | | final AttributeType attrType = indexConfig.getAttribute(); |
| | | final String indexID = indexType.toString(); |
| | | final MatchingRule rule = getMatchingRule(indexType, attrType); |
| | | if (rule == null) |
| | | { |
| | | throw new ConfigException(ERR_CONFIG_INDEX_TYPE_NEEDS_MATCHING_RULE.get(attrType, indexID)); |
| | | } |
| | | |
| | | for (org.forgerock.opendj.ldap.spi.Indexer indexer : rule.getIndexers()) |
| | | { |
| | | nameToIndexes.put(indexID, newAttributeIndex(indexConfig, indexer)); |
| | | } |
| | | } |
| | | } |
| | | |
| | | private MatchingRule getMatchingRule(IndexType indexType, AttributeType attrType) |
| | | { |
| | | switch (indexType) |
| | | { |
| | | case APPROXIMATE: |
| | | return attrType.getApproximateMatchingRule(); |
| | | case EQUALITY: |
| | | return attrType.getEqualityMatchingRule(); |
| | | case ORDERING: |
| | | return attrType.getOrderingMatchingRule(); |
| | | case SUBSTRING: |
| | | return attrType.getSubstringMatchingRule(); |
| | | default: |
| | | throw new IllegalArgumentException("Not implemented for index type " + indexType); |
| | | } |
| | | } |
| | | |
| | | private Index newAttributeIndex(LocalDBIndexCfg indexConfig, org.forgerock.opendj.ldap.spi.Indexer indexer) |
| | | { |
| | | final AttributeType attrType = indexConfig.getAttribute(); |
| | | final TreeName indexName = getIndexName(attrType, indexer.getIndexID()); |
| | | final AttributeIndexer attrIndexer = new AttributeIndexer(attrType, indexer); |
| | | return entryContainer.newIndexForAttribute(indexName, attrIndexer, indexConfig.getIndexEntryLimit()); |
| | | } |
| | | |
| | | private TreeName getIndexName(AttributeType attrType, String indexID) |
| | | { |
| | | return entryContainer.getDatabasePrefix().child(attrType.getNameOrOID() + "." + indexID); |
| | | } |
| | | |
| | | /** |
| | | * Open the attribute index. |
| | | * |
| | | * @throws StorageRuntimeException if a JE database error occurs while |
| | | * opening the index. |
| | | */ |
| | | public void open() throws StorageRuntimeException |
| | | { |
| | | for (Index index : nameToIndexes.values()) |
| | | { |
| | | index.open(); |
| | | } |
| | | indexConfig.addChangeListener(this); |
| | | } |
| | | |
| | | /** Closes the attribute index. */ |
| | | @Override |
| | | public void close() |
| | | { |
| | | Utils.closeSilently(nameToIndexes.values()); |
| | | indexConfig.removeChangeListener(this); |
| | | // The entryContainer is responsible for closing the JE databases. |
| | | } |
| | | |
| | | /** |
| | | * Get the attribute type of this attribute index. |
| | | * @return The attribute type of this attribute index. |
| | | */ |
| | | public AttributeType getAttributeType() |
| | | { |
| | | return indexConfig.getAttribute(); |
| | | } |
| | | |
| | | /** |
| | | * Return the indexing options of this AttributeIndex. |
| | | * |
| | | * @return the indexing options of this AttributeIndex. |
| | | */ |
| | | public IndexingOptions getIndexingOptions() |
| | | { |
| | | return indexQueryFactory.getIndexingOptions(); |
| | | } |
| | | |
| | | /** |
| | | * Get the JE index configuration used by this index. |
| | | * @return The configuration in effect. |
| | | */ |
| | | public LocalDBIndexCfg getConfiguration() |
| | | { |
| | | return indexConfig; |
| | | } |
| | | |
| | | /** |
| | | * Update the attribute index for a new entry. |
| | | * |
| | | * @param buffer The index buffer to use to store the added keys |
| | | * @param entryID The entry ID. |
| | | * @param entry The contents of the new entry. |
| | | * @throws StorageRuntimeException If an error occurs in the JE database. |
| | | * @throws DirectoryException If a Directory Server error occurs. |
| | | */ |
| | | public void addEntry(IndexBuffer buffer, EntryID entryID, Entry entry) |
| | | throws StorageRuntimeException, DirectoryException |
| | | { |
| | | final IndexingOptions options = indexQueryFactory.getIndexingOptions(); |
| | | for (Index index : nameToIndexes.values()) |
| | | { |
| | | index.addEntry(buffer, entryID, entry, options); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Update the attribute index for a deleted entry. |
| | | * |
| | | * @param buffer The index buffer to use to store the deleted keys |
| | | * @param entryID The entry ID |
| | | * @param entry The contents of the deleted entry. |
| | | * @throws StorageRuntimeException If an error occurs in the JE database. |
| | | * @throws DirectoryException If a Directory Server error occurs. |
| | | */ |
| | | public void removeEntry(IndexBuffer buffer, EntryID entryID, Entry entry) |
| | | throws StorageRuntimeException, DirectoryException |
| | | { |
| | | final IndexingOptions options = indexQueryFactory.getIndexingOptions(); |
| | | for (Index index : nameToIndexes.values()) |
| | | { |
| | | index.removeEntry(buffer, entryID, entry, options); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Update the index to reflect a sequence of modifications in a Modify |
| | | * operation. |
| | | * |
| | | * @param buffer The index buffer used to buffer up the index changes. |
| | | * @param entryID The ID of the entry that was modified. |
| | | * @param oldEntry The entry before the modifications were applied. |
| | | * @param newEntry The entry after the modifications were applied. |
| | | * @param mods The sequence of modifications in the Modify operation. |
| | | * @throws StorageRuntimeException If an error occurs during an operation on a |
| | | * JE database. |
| | | */ |
| | | public void modifyEntry(IndexBuffer buffer, |
| | | EntryID entryID, |
| | | Entry oldEntry, |
| | | Entry newEntry, |
| | | List<Modification> mods) |
| | | throws StorageRuntimeException |
| | | { |
| | | final IndexingOptions options = indexQueryFactory.getIndexingOptions(); |
| | | for (Index index : nameToIndexes.values()) |
| | | { |
| | | index.modifyEntry(buffer, entryID, oldEntry, newEntry, mods, options); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Decompose an attribute value into a set of substring index keys. |
| | | * The ID of the entry containing this value should be inserted |
| | | * into the list of each of these keys. |
| | | * |
| | | * @param normValue A byte array containing the normalized attribute value. |
| | | * @return A set of index keys. |
| | | */ |
| | | Set<ByteString> substringKeys(ByteString normValue) |
| | | { // FIXME replace this code with SDK's |
| | | // AbstractSubstringMatchingRuleImpl.SubstringIndexer.createKeys() |
| | | |
| | | // Eliminate duplicates by putting the keys into a set. |
| | | // Sorting the keys will ensure database record locks are acquired |
| | | // in a consistent order and help prevent transaction deadlocks between |
| | | // concurrent writers. |
| | | Set<ByteString> set = new HashSet<ByteString>(); |
| | | |
| | | int substrLength = indexConfig.getSubstringLength(); |
| | | |
| | | // Example: The value is ABCDE and the substring length is 3. |
| | | // We produce the keys ABC BCD CDE DE E |
| | | // To find values containing a short substring such as DE, |
| | | // iterate through keys with prefix DE. To find values |
| | | // containing a longer substring such as BCDE, read keys BCD and CDE. |
| | | for (int i = 0, remain = normValue.length(); remain > 0; i++, remain--) |
| | | { |
| | | int len = Math.min(substrLength, remain); |
| | | set.add(normValue.subSequence(i, i + len)); |
| | | } |
| | | return set; |
| | | } |
| | | |
| | | /** |
| | | * Retrieve the entry IDs that might match the provided assertion. |
| | | * |
| | | * @param indexQuery |
| | | * The query used to retrieve entries. |
| | | * @param indexName |
| | | * The name of index used to retrieve entries. |
| | | * @param filter |
| | | * The filter on entries. |
| | | * @param debugBuffer |
| | | * If not null, a diagnostic string will be written which will help |
| | | * determine how the indexes contributed to this search. |
| | | * @param monitor |
| | | * The database environment monitor provider that will keep index |
| | | * filter usage statistics. |
| | | * @return The candidate entry IDs that might contain the filter assertion |
| | | * value. |
| | | */ |
| | | private EntryIDSet evaluateIndexQuery(IndexQuery indexQuery, String indexName, SearchFilter filter, |
| | | StringBuilder debugBuffer, DatabaseEnvironmentMonitor monitor) |
| | | { |
| | | LocalizableMessageBuilder debugMessage = monitor.isFilterUseEnabled() ? new LocalizableMessageBuilder() : null; |
| | | EntryIDSet results = indexQuery.evaluate(debugMessage); |
| | | |
| | | if (debugBuffer != null) |
| | | { |
| | | debugBuffer.append("[INDEX:").append(indexConfig.getAttribute().getNameOrOID()) |
| | | .append(".").append(indexName).append("]"); |
| | | } |
| | | |
| | | if (monitor.isFilterUseEnabled()) |
| | | { |
| | | if (results.isDefined()) |
| | | { |
| | | monitor.updateStats(filter, results.size()); |
| | | } |
| | | else |
| | | { |
| | | monitor.updateStats(filter, debugMessage.toMessage()); |
| | | } |
| | | } |
| | | return results; |
| | | } |
| | | |
| | | /** |
| | | * Retrieve the entry IDs that might match two filters that restrict a value |
| | | * to both a lower bound and an upper bound. |
| | | * |
| | | * @param filter1 |
| | | * The first filter, that is either a less-or-equal filter or a |
| | | * greater-or-equal filter. |
| | | * @param filter2 |
| | | * The second filter, that is either a less-or-equal filter or a |
| | | * greater-or-equal filter. It must not be of the same type than the |
| | | * first filter. |
| | | * @param debugBuffer |
| | | * If not null, a diagnostic string will be written which will help |
| | | * determine how the indexes contributed to this search. |
| | | * @param monitor |
| | | * The database environment monitor provider that will keep index |
| | | * filter usage statistics. |
| | | * @return The candidate entry IDs that might contain match both filters. |
| | | */ |
| | | public EntryIDSet evaluateBoundedRange(SearchFilter filter1, SearchFilter filter2, StringBuilder debugBuffer, |
| | | DatabaseEnvironmentMonitor monitor) |
| | | { |
| | | // TODO : this implementation is not optimal |
| | | // as it implies two separate evaluations instead of a single one, |
| | | // thus defeating the purpose of the optimization done |
| | | // in IndexFilter#evaluateLogicalAndFilter method. |
| | | // One solution could be to implement a boundedRangeAssertion that combine |
| | | // the two operations in one. |
| | | EntryIDSet results = evaluate(filter1, debugBuffer, monitor); |
| | | EntryIDSet results2 = evaluate(filter2, debugBuffer, monitor); |
| | | results.retainAll(results2); |
| | | return results; |
| | | } |
| | | |
| | | private EntryIDSet evaluate(SearchFilter filter, StringBuilder debugBuffer, DatabaseEnvironmentMonitor monitor) |
| | | { |
| | | boolean isLessOrEqual = filter.getFilterType() == FilterType.LESS_OR_EQUAL; |
| | | IndexFilterType indexFilterType = isLessOrEqual ? IndexFilterType.LESS_OR_EQUAL : IndexFilterType.GREATER_OR_EQUAL; |
| | | return evaluateFilter(indexFilterType, filter, debugBuffer, monitor); |
| | | } |
| | | |
| | | /** |
| | | * Retrieve the entry IDs that might match a filter. |
| | | * |
| | | * @param indexFilterType the index type filter |
| | | * @param filter The filter. |
| | | * @param debugBuffer If not null, a diagnostic string will be written |
| | | * which will help determine how the indexes contributed |
| | | * to this search. |
| | | * @param monitor The database environment monitor provider that will keep |
| | | * index filter usage statistics. |
| | | * @return The candidate entry IDs that might contain a value |
| | | * that matches the filter type. |
| | | */ |
| | | public EntryIDSet evaluateFilter(IndexFilterType indexFilterType, SearchFilter filter, StringBuilder debugBuffer, |
| | | DatabaseEnvironmentMonitor monitor) |
| | | { |
| | | try |
| | | { |
| | | final IndexQuery indexQuery = getIndexQuery(indexFilterType, filter); |
| | | return evaluateIndexQuery(indexQuery, indexFilterType.toString(), filter, debugBuffer, monitor); |
| | | } |
| | | catch (DecodeException e) |
| | | { |
| | | logger.traceException(e); |
| | | return new EntryIDSet(); |
| | | } |
| | | } |
| | | |
| | | private IndexQuery getIndexQuery(IndexFilterType indexFilterType, SearchFilter filter) throws DecodeException |
| | | { |
| | | MatchingRule rule; |
| | | Assertion assertion; |
| | | switch (indexFilterType) |
| | | { |
| | | case EQUALITY: |
| | | rule = filter.getAttributeType().getEqualityMatchingRule(); |
| | | assertion = rule.getAssertion(filter.getAssertionValue()); |
| | | return assertion.createIndexQuery(indexQueryFactory); |
| | | |
| | | case PRESENCE: |
| | | return indexQueryFactory.createMatchAllQuery(); |
| | | |
| | | case GREATER_OR_EQUAL: |
| | | rule = filter.getAttributeType().getOrderingMatchingRule(); |
| | | assertion = rule.getGreaterOrEqualAssertion(filter.getAssertionValue()); |
| | | return assertion.createIndexQuery(indexQueryFactory); |
| | | |
| | | case LESS_OR_EQUAL: |
| | | rule = filter.getAttributeType().getOrderingMatchingRule(); |
| | | assertion = rule.getLessOrEqualAssertion(filter.getAssertionValue()); |
| | | return assertion.createIndexQuery(indexQueryFactory); |
| | | |
| | | case SUBSTRING: |
| | | rule = filter.getAttributeType().getSubstringMatchingRule(); |
| | | assertion = rule.getSubstringAssertion( |
| | | filter.getSubInitialElement(), filter.getSubAnyElements(), filter.getSubFinalElement()); |
| | | return assertion.createIndexQuery(indexQueryFactory); |
| | | |
| | | case APPROXIMATE: |
| | | rule = filter.getAttributeType().getApproximateMatchingRule(); |
| | | assertion = rule.getAssertion(filter.getAssertionValue()); |
| | | return assertion.createIndexQuery(indexQueryFactory); |
| | | |
| | | default: |
| | | return null; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Return the number of values that have exceeded the entry limit since this |
| | | * object was created. |
| | | * |
| | | * @return The number of values that have exceeded the entry limit. |
| | | */ |
| | | public long getEntryLimitExceededCount() |
| | | { |
| | | long entryLimitExceededCount = 0; |
| | | |
| | | for (Index index : nameToIndexes.values()) |
| | | { |
| | | entryLimitExceededCount += index.getEntryLimitExceededCount(); |
| | | } |
| | | return entryLimitExceededCount; |
| | | } |
| | | |
| | | /** |
| | | * Get a list of the databases opened by this attribute index. |
| | | * @param dbList A list of database containers. |
| | | */ |
| | | public void listDatabases(List<DatabaseContainer> dbList) |
| | | { |
| | | dbList.addAll(nameToIndexes.values()); |
| | | } |
| | | |
| | | /** |
| | | * Get a string representation of this object. |
| | | * @return return A string representation of this object. |
| | | */ |
| | | @Override |
| | | public String toString() |
| | | { |
| | | return getName(); |
| | | } |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public synchronized boolean isConfigurationChangeAcceptable( |
| | | LocalDBIndexCfg cfg, List<LocalizableMessage> unacceptableReasons) |
| | | { |
| | | if (!isIndexAcceptable(cfg, IndexType.EQUALITY, unacceptableReasons) |
| | | || !isIndexAcceptable(cfg, IndexType.SUBSTRING, unacceptableReasons) |
| | | || !isIndexAcceptable(cfg, IndexType.ORDERING, unacceptableReasons) |
| | | || !isIndexAcceptable(cfg, IndexType.APPROXIMATE, unacceptableReasons)) |
| | | { |
| | | return false; |
| | | } |
| | | |
| | | AttributeType attrType = cfg.getAttribute(); |
| | | if (cfg.getIndexType().contains(IndexType.EXTENSIBLE)) |
| | | { |
| | | Set<String> newRules = cfg.getIndexExtensibleMatchingRule(); |
| | | if (newRules == null || newRules.isEmpty()) |
| | | { |
| | | unacceptableReasons.add(ERR_CONFIG_INDEX_TYPE_NEEDS_MATCHING_RULE.get(attrType, "extensible")); |
| | | return false; |
| | | } |
| | | } |
| | | return true; |
| | | } |
| | | |
| | | private boolean isIndexAcceptable(LocalDBIndexCfg cfg, IndexType indexType, |
| | | List<LocalizableMessage> unacceptableReasons) |
| | | { |
| | | final String indexId = indexType.toString(); |
| | | final AttributeType attrType = cfg.getAttribute(); |
| | | if (cfg.getIndexType().contains(indexType) |
| | | && nameToIndexes.get(indexId) == null |
| | | && getMatchingRule(indexType, attrType) == null) |
| | | { |
| | | unacceptableReasons.add(ERR_CONFIG_INDEX_TYPE_NEEDS_MATCHING_RULE.get(attrType, indexId)); |
| | | return false; |
| | | } |
| | | return true; |
| | | } |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public synchronized ConfigChangeResult applyConfigurationChange(LocalDBIndexCfg cfg) |
| | | { |
| | | // this method is not perf sensitive, using an AtomicBoolean will not hurt |
| | | AtomicBoolean adminActionRequired = new AtomicBoolean(false); |
| | | ArrayList<LocalizableMessage> messages = new ArrayList<LocalizableMessage>(); |
| | | try |
| | | { |
| | | applyChangeToPresenceIndex(cfg, adminActionRequired, messages); |
| | | applyChangeToIndex(IndexType.EQUALITY, cfg, adminActionRequired, messages); |
| | | applyChangeToIndex(IndexType.SUBSTRING, cfg, adminActionRequired, messages); |
| | | applyChangeToIndex(IndexType.ORDERING, cfg, adminActionRequired, messages); |
| | | applyChangeToIndex(IndexType.APPROXIMATE, cfg, adminActionRequired, messages); |
| | | applyChangeToExtensibleIndexes(cfg, adminActionRequired, messages); |
| | | |
| | | extensibleIndexesMapping = computeExtensibleIndexesMapping(); |
| | | indexConfig = cfg; |
| | | |
| | | return new ConfigChangeResult(ResultCode.SUCCESS, adminActionRequired.get(), messages); |
| | | } |
| | | catch(Exception e) |
| | | { |
| | | messages.add(LocalizableMessage.raw(StaticUtils.stackTraceToSingleLineString(e))); |
| | | return new ConfigChangeResult( |
| | | DirectoryServer.getServerErrorResultCode(), adminActionRequired.get(), messages); |
| | | } |
| | | } |
| | | |
| | | private void applyChangeToExtensibleIndexes(LocalDBIndexCfg cfg, |
| | | AtomicBoolean adminActionRequired, ArrayList<LocalizableMessage> messages) |
| | | { |
| | | final AttributeType attrType = cfg.getAttribute(); |
| | | if (!cfg.getIndexType().contains(IndexType.EXTENSIBLE)) |
| | | { |
| | | final Set<MatchingRule> validRules = Collections.emptySet(); |
| | | final Set<String> validIndexIds = Collections.emptySet(); |
| | | removeIndexesForExtensibleMatchingRules(validRules, validIndexIds); |
| | | return; |
| | | } |
| | | |
| | | final Set<String> extensibleRules = cfg.getIndexExtensibleMatchingRule(); |
| | | final Set<MatchingRule> validRules = new HashSet<MatchingRule>(); |
| | | final Set<String> validIndexIds = new HashSet<String>(); |
| | | final int indexEntryLimit = cfg.getIndexEntryLimit(); |
| | | |
| | | for (String ruleName : extensibleRules) |
| | | { |
| | | MatchingRule rule = DirectoryServer.getMatchingRule(toLowerCase(ruleName)); |
| | | if (rule == null) |
| | | { |
| | | logger.error(ERR_CONFIG_INDEX_TYPE_NEEDS_VALID_MATCHING_RULE, attrType, ruleName); |
| | | continue; |
| | | } |
| | | validRules.add(rule); |
| | | for (org.forgerock.opendj.ldap.spi.Indexer indexer : rule.getIndexers()) |
| | | { |
| | | String indexId = indexer.getIndexID(); |
| | | validIndexIds.add(indexId); |
| | | if (!nameToIndexes.containsKey(indexId)) |
| | | { |
| | | Index index = newAttributeIndex(cfg, indexer); |
| | | openIndex(index, adminActionRequired, messages); |
| | | nameToIndexes.put(indexId, index); |
| | | } |
| | | else |
| | | { |
| | | Index index = nameToIndexes.get(indexId); |
| | | if (index.setIndexEntryLimit(indexEntryLimit)) |
| | | { |
| | | adminActionRequired.set(true); |
| | | messages.add(NOTE_JEB_CONFIG_INDEX_ENTRY_LIMIT_REQUIRES_REBUILD.get(index.getName())); |
| | | } |
| | | if (indexConfig.getSubstringLength() != cfg.getSubstringLength()) |
| | | { |
| | | index.setIndexer(new AttributeIndexer(attrType, indexer)); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | removeIndexesForExtensibleMatchingRules(validRules, validIndexIds); |
| | | } |
| | | |
| | | /** Remove indexes which do not correspond to valid rules. */ |
| | | private void removeIndexesForExtensibleMatchingRules(Set<MatchingRule> validRules, Set<String> validIndexIds) |
| | | { |
| | | final Set<MatchingRule> rulesToDelete = getCurrentExtensibleMatchingRules(); |
| | | rulesToDelete.removeAll(validRules); |
| | | if (!rulesToDelete.isEmpty()) |
| | | { |
| | | entryContainer.exclusiveLock.lock(); |
| | | try |
| | | { |
| | | for (MatchingRule rule: rulesToDelete) |
| | | { |
| | | final List<String> indexIdsToRemove = new ArrayList<String>(); |
| | | for (org.forgerock.opendj.ldap.spi.Indexer indexer : rule.getIndexers()) |
| | | { |
| | | final String indexId = indexer.getIndexID(); |
| | | if (!validIndexIds.contains(indexId)) |
| | | { |
| | | indexIdsToRemove.add(indexId); |
| | | } |
| | | } |
| | | // Delete indexes which are not used |
| | | for (String indexId : indexIdsToRemove) |
| | | { |
| | | Index index = nameToIndexes.get(indexId); |
| | | if (index != null) |
| | | { |
| | | entryContainer.deleteDatabase(index); |
| | | nameToIndexes.remove(index); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | finally |
| | | { |
| | | entryContainer.exclusiveLock.unlock(); |
| | | } |
| | | } |
| | | } |
| | | |
| | | private Set<MatchingRule> getCurrentExtensibleMatchingRules() |
| | | { |
| | | final Set<MatchingRule> rules = new HashSet<MatchingRule>(); |
| | | for (String ruleName : indexConfig.getIndexExtensibleMatchingRule()) |
| | | { |
| | | final MatchingRule rule = DirectoryServer.getMatchingRule(toLowerCase(ruleName)); |
| | | if (rule != null) |
| | | { |
| | | rules.add(rule); |
| | | } |
| | | } |
| | | return rules; |
| | | } |
| | | |
| | | private void applyChangeToIndex(IndexType indexType, LocalDBIndexCfg cfg, |
| | | AtomicBoolean adminActionRequired, ArrayList<LocalizableMessage> messages) |
| | | { |
| | | String indexId = indexType.toString(); |
| | | Index index = nameToIndexes.get(indexId); |
| | | if (!cfg.getIndexType().contains(indexType)) |
| | | { |
| | | removeIndex(index, indexType); |
| | | return; |
| | | } |
| | | |
| | | if (index == null) |
| | | { |
| | | final MatchingRule matchingRule = getMatchingRule(indexType, cfg.getAttribute()); |
| | | for (org.forgerock.opendj.ldap.spi.Indexer indexer : matchingRule.getIndexers()) |
| | | { |
| | | index = newAttributeIndex(cfg, indexer); |
| | | openIndex(index, adminActionRequired, messages); |
| | | nameToIndexes.put(indexId, index); |
| | | } |
| | | } |
| | | else |
| | | { |
| | | // already exists. Just update index entry limit. |
| | | if (index.setIndexEntryLimit(cfg.getIndexEntryLimit())) |
| | | { |
| | | adminActionRequired.set(true); |
| | | messages.add(NOTE_JEB_CONFIG_INDEX_ENTRY_LIMIT_REQUIRES_REBUILD.get(index.getName())); |
| | | } |
| | | } |
| | | } |
| | | |
| | | private void applyChangeToPresenceIndex(LocalDBIndexCfg cfg, AtomicBoolean adminActionRequired, |
| | | ArrayList<LocalizableMessage> messages) |
| | | { |
| | | final IndexType indexType = IndexType.PRESENCE; |
| | | final String indexID = indexType.toString(); |
| | | Index index = nameToIndexes.get(indexID); |
| | | if (!cfg.getIndexType().contains(indexType)) |
| | | { |
| | | removeIndex(index, indexType); |
| | | return; |
| | | } |
| | | |
| | | if (index == null) |
| | | { |
| | | index = newPresenceIndex(cfg); |
| | | openIndex(index, adminActionRequired, messages); |
| | | nameToIndexes.put(indexID, index); |
| | | } |
| | | else |
| | | { |
| | | // already exists. Just update index entry limit. |
| | | if (index.setIndexEntryLimit(cfg.getIndexEntryLimit())) |
| | | { |
| | | adminActionRequired.set(true); |
| | | messages.add(NOTE_JEB_CONFIG_INDEX_ENTRY_LIMIT_REQUIRES_REBUILD.get(index.getName())); |
| | | } |
| | | } |
| | | } |
| | | |
| | | private void removeIndex(Index index, IndexType indexType) |
| | | { |
| | | if (index != null) |
| | | { |
| | | entryContainer.exclusiveLock.lock(); |
| | | try |
| | | { |
| | | nameToIndexes.remove(indexType.toString()); |
| | | entryContainer.deleteDatabase(index); |
| | | } |
| | | finally |
| | | { |
| | | entryContainer.exclusiveLock.unlock(); |
| | | } |
| | | } |
| | | } |
| | | |
| | | private void openIndex(Index index, AtomicBoolean adminActionRequired, ArrayList<LocalizableMessage> messages) |
| | | { |
| | | index.open(); |
| | | |
| | | if (!index.isTrusted()) |
| | | { |
| | | adminActionRequired.set(true); |
| | | messages.add(NOTE_JEB_INDEX_ADD_REQUIRES_REBUILD.get(index.getName())); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Return true iff this index is trusted. |
| | | * @return the trusted state of this index |
| | | */ |
| | | public boolean isTrusted() |
| | | { |
| | | for (Index index : nameToIndexes.values()) |
| | | { |
| | | if (!index.isTrusted()) |
| | | { |
| | | return false; |
| | | } |
| | | } |
| | | return true; |
| | | } |
| | | |
| | | /** |
| | | * Set the rebuild status of this index. |
| | | * @param rebuildRunning True if a rebuild process on this index |
| | | * is running or False otherwise. |
| | | */ |
| | | public synchronized void setRebuildStatus(boolean rebuildRunning) |
| | | { |
| | | for (Index index : nameToIndexes.values()) |
| | | { |
| | | index.setRebuildStatus(rebuildRunning); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Get the JE database name prefix for indexes in this attribute index. |
| | | * |
| | | * @return JE database name for this database container. |
| | | */ |
| | | public String getName() |
| | | { |
| | | return entryContainer.getDatabasePrefix() |
| | | + "_" |
| | | + indexConfig.getAttribute().getNameOrOID(); |
| | | } |
| | | |
| | | /** |
| | | * Return the equality index. |
| | | * |
| | | * @return The equality index. |
| | | */ |
| | | public Index getEqualityIndex() { |
| | | return nameToIndexes.get(IndexType.EQUALITY.toString()); |
| | | } |
| | | |
| | | /** |
| | | * Return the approximate index. |
| | | * |
| | | * @return The approximate index. |
| | | */ |
| | | public Index getApproximateIndex() { |
| | | return nameToIndexes.get(IndexType.APPROXIMATE.toString()); |
| | | } |
| | | |
| | | /** |
| | | * Return the ordering index. |
| | | * |
| | | * @return The ordering index. |
| | | */ |
| | | public Index getOrderingIndex() { |
| | | return nameToIndexes.get(IndexType.ORDERING.toString()); |
| | | } |
| | | |
| | | /** |
| | | * Return the substring index. |
| | | * |
| | | * @return The substring index. |
| | | */ |
| | | public Index getSubstringIndex() { |
| | | return nameToIndexes.get(IndexType.SUBSTRING.toString()); |
| | | } |
| | | |
| | | /** |
| | | * Return the presence index. |
| | | * |
| | | * @return The presence index. |
| | | */ |
| | | public Index getPresenceIndex() { |
| | | return nameToIndexes.get(IndexType.PRESENCE.toString()); |
| | | } |
| | | |
| | | /** |
| | | * Return the mapping of extensible index types and indexes. |
| | | * |
| | | * @return The map containing entries (extensible index type, list of indexes) |
| | | */ |
| | | public Map<String, Collection<Index>> getExtensibleIndexes() |
| | | { |
| | | return extensibleIndexesMapping; |
| | | } |
| | | |
| | | private Map<String, Collection<Index>> computeExtensibleIndexesMapping() |
| | | { |
| | | final Collection<Index> substring = new ArrayList<Index>(); |
| | | final Collection<Index> shared = new ArrayList<Index>(); |
| | | for (Map.Entry<String, Index> entry : nameToIndexes.entrySet()) |
| | | { |
| | | final String indexId = entry.getKey(); |
| | | if (isDefaultIndex(indexId)) { |
| | | continue; |
| | | } |
| | | if (indexId.endsWith(EXTENSIBLE_INDEXER_ID_SUBSTRING)) |
| | | { |
| | | substring.add(entry.getValue()); |
| | | } |
| | | else |
| | | { |
| | | shared.add(entry.getValue()); |
| | | } |
| | | } |
| | | final Map<String, Collection<Index>> indexMap = new HashMap<String,Collection<Index>>(); |
| | | indexMap.put(EXTENSIBLE_INDEXER_ID_SUBSTRING, substring); |
| | | indexMap.put(EXTENSIBLE_INDEXER_ID_SHARED, shared); |
| | | return Collections.unmodifiableMap(indexMap); |
| | | } |
| | | |
| | | private boolean isDefaultIndex(String indexId) |
| | | { |
| | | return indexId.equals(IndexType.EQUALITY.toString()) |
| | | || indexId.equals(IndexType.PRESENCE.toString()) |
| | | || indexId.equals(IndexType.SUBSTRING.toString()) |
| | | || indexId.equals(IndexType.ORDERING.toString()) |
| | | || indexId.equals(IndexType.APPROXIMATE.toString()); |
| | | } |
| | | |
| | | /** |
| | | * Retrieves all the indexes used by this attribute index. |
| | | * |
| | | * @return A collection of all indexes in use by this attribute |
| | | * index. |
| | | */ |
| | | public Collection<Index> getAllIndexes() { |
| | | return new LinkedHashSet<Index>(nameToIndexes.values()); |
| | | } |
| | | |
| | | /** |
| | | * Retrieve the entry IDs that might match an extensible filter. |
| | | * |
| | | * @param filter The extensible filter. |
| | | * @param debugBuffer If not null, a diagnostic string will be written |
| | | * which will help determine how the indexes contributed |
| | | * to this search. |
| | | * @param monitor The database environment monitor provider that will keep |
| | | * index filter usage statistics. |
| | | * @return The candidate entry IDs that might contain the filter |
| | | * assertion value. |
| | | */ |
| | | public EntryIDSet evaluateExtensibleFilter(SearchFilter filter, |
| | | StringBuilder debugBuffer, |
| | | DatabaseEnvironmentMonitor monitor) |
| | | { |
| | | //Get the Matching Rule OID of the filter. |
| | | String matchRuleOID = filter.getMatchingRuleID(); |
| | | /** |
| | | * Use the default equality index in two conditions: |
| | | * 1. There is no matching rule provided |
| | | * 2. The matching rule specified is actually the default equality. |
| | | */ |
| | | MatchingRule eqRule = indexConfig.getAttribute().getEqualityMatchingRule(); |
| | | if (matchRuleOID == null |
| | | || matchRuleOID.equals(eqRule.getOID()) |
| | | || matchRuleOID.equalsIgnoreCase(eqRule.getNameOrOID())) |
| | | { |
| | | //No matching rule is defined; use the default equality matching rule. |
| | | return evaluateFilter(IndexFilterType.EQUALITY, filter, debugBuffer, monitor); |
| | | } |
| | | |
| | | MatchingRule rule = DirectoryServer.getMatchingRule(matchRuleOID); |
| | | if (!ruleHasAtLeasOneIndex(rule)) |
| | | { |
| | | if (monitor.isFilterUseEnabled()) |
| | | { |
| | | monitor.updateStats(filter, INFO_JEB_INDEX_FILTER_MATCHING_RULE_NOT_INDEXED.get( |
| | | matchRuleOID, indexConfig.getAttribute().getNameOrOID())); |
| | | } |
| | | return IndexQuery.createNullIndexQuery().evaluate(null); |
| | | } |
| | | |
| | | try |
| | | { |
| | | if (debugBuffer != null) |
| | | { |
| | | debugBuffer.append("[INDEX:"); |
| | | for (org.forgerock.opendj.ldap.spi.Indexer indexer : rule.getIndexers()) |
| | | { |
| | | debugBuffer.append(" ") |
| | | .append(filter.getAttributeType().getNameOrOID()) |
| | | .append(".") |
| | | .append(indexer.getIndexID()); |
| | | } |
| | | debugBuffer.append("]"); |
| | | } |
| | | |
| | | final IndexQuery indexQuery = rule.getAssertion(filter.getAssertionValue()).createIndexQuery(indexQueryFactory); |
| | | LocalizableMessageBuilder debugMessage = monitor.isFilterUseEnabled() ? new LocalizableMessageBuilder() : null; |
| | | EntryIDSet results = indexQuery.evaluate(debugMessage); |
| | | if (monitor.isFilterUseEnabled()) |
| | | { |
| | | if (results.isDefined()) |
| | | { |
| | | monitor.updateStats(filter, results.size()); |
| | | } |
| | | else |
| | | { |
| | | monitor.updateStats(filter, debugMessage.toMessage()); |
| | | } |
| | | } |
| | | return results; |
| | | } |
| | | catch (DecodeException e) |
| | | { |
| | | logger.traceException(e); |
| | | return IndexQuery.createNullIndexQuery().evaluate(null); |
| | | } |
| | | } |
| | | |
| | | private boolean ruleHasAtLeasOneIndex(MatchingRule rule) |
| | | { |
| | | for (org.forgerock.opendj.ldap.spi.Indexer indexer : rule.getIndexers()) |
| | | { |
| | | if (nameToIndexes.containsKey(indexer.getIndexID())) |
| | | { |
| | | return true; |
| | | } |
| | | } |
| | | return false; |
| | | } |
| | | |
| | | /** |
| | | * This class extends the IndexConfig for JE Backend. |
| | | */ |
| | | private class JEIndexConfig implements IndexingOptions |
| | | { |
| | | /** The length of the substring index. */ |
| | | private int substringLength; |
| | | |
| | | /** |
| | | * Creates a new JEIndexConfig instance. |
| | | * @param substringLength The length of the substring. |
| | | */ |
| | | private JEIndexConfig(int substringLength) |
| | | { |
| | | this.substringLength = substringLength; |
| | | } |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public int substringKeySize() |
| | | { |
| | | return substringLength; |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | /* |
| | | * CDDL HEADER START |
| | | * |
| | | * The contents of this file are subject to the terms of the |
| | | * Common Development and Distribution License, Version 1.0 only |
| | | * (the "License"). You may not use this file except in compliance |
| | | * with the License. |
| | | * |
| | | * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt |
| | | * or http://forgerock.org/license/CDDLv1.0.html. |
| | | * See the License for the specific language governing permissions |
| | | * and limitations under the License. |
| | | * |
| | | * When distributing Covered Code, include this CDDL HEADER in each |
| | | * file and include the License file at legal-notices/CDDLv1_0.txt. |
| | | * If applicable, add the following below this CDDL HEADER, with the |
| | | * fields enclosed by brackets "[]" replaced with your own identifying |
| | | * information: |
| | | * Portions Copyright [yyyy] [name of copyright owner] |
| | | * |
| | | * CDDL HEADER END |
| | | * |
| | | * |
| | | * Copyright 2009-2010 Sun Microsystems, Inc. |
| | | * Portions Copyright 2014 ForgeRock AS |
| | | */ |
| | | package org.opends.server.backends.pluggable; |
| | | |
| | | import java.util.HashSet; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import java.util.Set; |
| | | |
| | | import org.forgerock.i18n.slf4j.LocalizedLogger; |
| | | import org.forgerock.opendj.ldap.ByteString; |
| | | import org.forgerock.opendj.ldap.DecodeException; |
| | | import org.forgerock.opendj.ldap.schema.Schema; |
| | | import org.forgerock.opendj.ldap.spi.IndexingOptions; |
| | | import org.opends.server.types.Attribute; |
| | | import org.opends.server.types.AttributeType; |
| | | import org.opends.server.types.Entry; |
| | | import org.opends.server.types.Modification; |
| | | |
| | | /** |
| | | * This class implements an attribute indexer for matching rules in JE Backend. |
| | | */ |
| | | public final class AttributeIndexer extends Indexer |
| | | { |
| | | private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); |
| | | |
| | | /** The attribute type for which this instance will generate index keys. */ |
| | | private final AttributeType attributeType; |
| | | |
| | | /** |
| | | * The indexer which will generate the keys |
| | | * for the associated extensible matching rule. |
| | | */ |
| | | private final org.forgerock.opendj.ldap.spi.Indexer indexer; |
| | | |
| | | /** |
| | | * Creates a new extensible indexer for JE backend. |
| | | * |
| | | * @param attributeType The attribute type for which an indexer is |
| | | * required. |
| | | * @param extensibleIndexer The extensible indexer to be used. |
| | | */ |
| | | public AttributeIndexer(AttributeType attributeType, org.forgerock.opendj.ldap.spi.Indexer extensibleIndexer) |
| | | { |
| | | this.attributeType = attributeType; |
| | | this.indexer = extensibleIndexer; |
| | | } |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public String toString() |
| | | { |
| | | return attributeType.getNameOrOID() + "." + indexer.getIndexID(); |
| | | } |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public void indexEntry(Entry entry, Set<ByteString> keys, IndexingOptions options) |
| | | { |
| | | List<Attribute> attrList = entry.getAttribute(attributeType); |
| | | if (attrList != null) |
| | | { |
| | | indexAttribute(attrList, keys, options); |
| | | } |
| | | } |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public void replaceEntry(Entry oldEntry, Entry newEntry, |
| | | Map<ByteString, Boolean> modifiedKeys, IndexingOptions options) |
| | | { |
| | | List<Attribute> newAttributes = newEntry.getAttribute(attributeType, true); |
| | | List<Attribute> oldAttributes = oldEntry.getAttribute(attributeType, true); |
| | | |
| | | indexAttribute(oldAttributes, modifiedKeys, false, options); |
| | | indexAttribute(newAttributes, modifiedKeys, true, options); |
| | | } |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public void modifyEntry(Entry oldEntry, Entry newEntry, |
| | | List<Modification> mods, Map<ByteString, Boolean> modifiedKeys, |
| | | IndexingOptions options) |
| | | { |
| | | List<Attribute> newAttributes = newEntry.getAttribute(attributeType, true); |
| | | List<Attribute> oldAttributes = oldEntry.getAttribute(attributeType, true); |
| | | |
| | | indexAttribute(oldAttributes, modifiedKeys, false, options); |
| | | indexAttribute(newAttributes, modifiedKeys, true, options); |
| | | } |
| | | |
| | | |
| | | |
| | | /** |
| | | * Generates the set of extensible index keys for an attribute. |
| | | * @param attrList The attribute for which substring keys are required. |
| | | * @param keys The set into which the generated keys will be inserted. |
| | | */ |
| | | private void indexAttribute(List<Attribute> attrList, Set<ByteString> keys, |
| | | IndexingOptions options) |
| | | { |
| | | if (attrList == null) |
| | | { |
| | | return; |
| | | } |
| | | |
| | | for (Attribute attr : attrList) |
| | | { |
| | | if (!attr.isVirtual()) |
| | | { |
| | | for (ByteString value : attr) |
| | | { |
| | | try |
| | | { |
| | | indexer.createKeys(Schema.getDefaultSchema(), value, options, keys); |
| | | } |
| | | catch (DecodeException e) |
| | | { |
| | | logger.traceException(e); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Generates the set of index keys for an attribute. |
| | | * @param attrList The attribute to be indexed. |
| | | * @param modifiedKeys The map into which the modified |
| | | * keys will be inserted. |
| | | * @param insert <code>true</code> if generated keys should |
| | | * be inserted or <code>false</code> otherwise. |
| | | */ |
| | | private void indexAttribute(List<Attribute> attrList, |
| | | Map<ByteString, Boolean> modifiedKeys, Boolean insert, |
| | | IndexingOptions options) |
| | | { |
| | | if (attrList == null) |
| | | { |
| | | return; |
| | | } |
| | | |
| | | final Set<ByteString> keys = new HashSet<ByteString>(); |
| | | indexAttribute(attrList, keys, options); |
| | | computeModifiedKeys(modifiedKeys, insert, keys); |
| | | } |
| | | |
| | | /** |
| | | * Computes a map of index keys and a boolean flag indicating whether the |
| | | * corresponding key will be inserted or deleted. |
| | | * |
| | | * @param modifiedKeys |
| | | * A map containing the keys and a boolean. Keys corresponding to the |
| | | * boolean value <code>true</code> should be inserted and |
| | | * <code>false</code> should be deleted. |
| | | * @param insert |
| | | * <code>true</code> if generated keys should be inserted or |
| | | * <code>false</code> otherwise. |
| | | * @param keys |
| | | * The index keys to map. |
| | | */ |
| | | private static void computeModifiedKeys(Map<ByteString, Boolean> modifiedKeys, |
| | | Boolean insert, Set<ByteString> keys) |
| | | { |
| | | for (ByteString key : keys) |
| | | { |
| | | Boolean cInsert = modifiedKeys.get(key); |
| | | if (cInsert == null) |
| | | { |
| | | modifiedKeys.put(key, insert); |
| | | } |
| | | else if (!cInsert.equals(insert)) |
| | | { |
| | | modifiedKeys.remove(key); |
| | | } |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | /* |
| | | * CDDL HEADER START |
| | | * |
| | | * The contents of this file are subject to the terms of the |
| | | * Common Development and Distribution License, Version 1.0 only |
| | | * (the "License"). You may not use this file except in compliance |
| | | * with the License. |
| | | * |
| | | * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt |
| | | * or http://forgerock.org/license/CDDLv1.0.html. |
| | | * See the License for the specific language governing permissions |
| | | * and limitations under the License. |
| | | * |
| | | * When distributing Covered Code, include this CDDL HEADER in each |
| | | * file and include the License file at legal-notices/CDDLv1_0.txt. |
| | | * If applicable, add the following below this CDDL HEADER, with the |
| | | * fields enclosed by brackets "[]" replaced with your own identifying |
| | | * information: |
| | | * Portions Copyright [yyyy] [name of copyright owner] |
| | | * |
| | | * CDDL HEADER END |
| | | * |
| | | * |
| | | * Copyright 2007-2010 Sun Microsystems, Inc. |
| | | * Portions Copyright 2013-2014 ForgeRock AS |
| | | */ |
| | | package org.opends.server.backends.pluggable; |
| | | |
| | | import java.io.Closeable; |
| | | import java.io.File; |
| | | import java.io.FileInputStream; |
| | | import java.io.FilenameFilter; |
| | | import java.io.IOException; |
| | | import java.util.*; |
| | | import java.util.concurrent.ExecutionException; |
| | | import java.util.concurrent.TimeUnit; |
| | | import java.util.concurrent.atomic.AtomicInteger; |
| | | import java.util.logging.Level; |
| | | import java.util.zip.Adler32; |
| | | import java.util.zip.CheckedInputStream; |
| | | |
| | | import org.forgerock.i18n.LocalizableMessage; |
| | | import org.forgerock.i18n.slf4j.LocalizedLogger; |
| | | import org.forgerock.opendj.config.server.ConfigException; |
| | | import org.forgerock.opendj.ldap.ByteSequence; |
| | | import org.forgerock.opendj.ldap.ByteString; |
| | | import org.forgerock.opendj.ldap.ConditionResult; |
| | | import org.forgerock.opendj.ldap.ResultCode; |
| | | import org.forgerock.util.Reject; |
| | | import org.opends.server.admin.server.ConfigurationChangeListener; |
| | | import org.opends.server.admin.std.meta.LocalDBIndexCfgDefn; |
| | | import org.opends.server.admin.std.server.LocalDBBackendCfg; |
| | | import org.opends.server.api.AlertGenerator; |
| | | import org.opends.server.api.Backend; |
| | | import org.opends.server.api.DiskSpaceMonitorHandler; |
| | | import org.opends.server.api.MonitorProvider; |
| | | import org.opends.server.core.*; |
| | | import org.opends.server.extensions.DiskSpaceMonitor; |
| | | import org.opends.server.types.*; |
| | | import org.opends.server.util.RuntimeInformation; |
| | | |
| | | import com.sleepycat.je.Durability; |
| | | import com.sleepycat.je.EnvironmentConfig; |
| | | |
| | | import static com.sleepycat.je.EnvironmentConfig.*; |
| | | |
| | | import static org.opends.messages.BackendMessages.*; |
| | | import static org.opends.messages.JebMessages.*; |
| | | import static org.opends.server.backends.jeb.ConfigurableEnvironment.*; |
| | | import static org.opends.server.util.ServerConstants.*; |
| | | import static org.opends.server.util.StaticUtils.*; |
| | | |
| | | /** |
| | | * This is an implementation of a Directory Server Backend which stores entries |
| | | * locally in a Berkeley DB JE database. |
| | | */ |
| | | public class BackendImpl extends Backend<LocalDBBackendCfg> |
| | | implements ConfigurationChangeListener<LocalDBBackendCfg>, AlertGenerator, |
| | | DiskSpaceMonitorHandler |
| | | { |
| | | private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); |
| | | |
| | | interface Importer extends Closeable |
| | | { |
| | | void createTree(TreeName name); |
| | | |
| | | void put(TreeName name, ByteSequence key, ByteSequence value); |
| | | |
| | | @Override |
| | | void close(); |
| | | } |
| | | |
| | | interface ReadOperation<T> |
| | | { |
| | | T run(ReadableStorage txn) throws Exception; |
| | | } |
| | | |
| | | interface ReadableStorage |
| | | { |
| | | ByteString get(TreeName name, ByteSequence key); |
| | | |
| | | ByteString getRMW(TreeName name, ByteSequence key); |
| | | |
| | | Cursor openCursor(TreeName name); |
| | | |
| | | // TODO: contains, etc. |
| | | } |
| | | |
| | | interface Cursor extends Closeable |
| | | { |
| | | boolean positionToKey(ByteSequence key); |
| | | |
| | | boolean positionToKeyOrNext(ByteSequence key); |
| | | |
| | | boolean positionToLastKey(); |
| | | |
| | | boolean next(); |
| | | |
| | | boolean previous(); |
| | | |
| | | ByteString getKey(); |
| | | |
| | | ByteString getValue(); |
| | | |
| | | @Override |
| | | public void close(); |
| | | } |
| | | |
| | | interface Storage extends Closeable |
| | | { |
| | | void initialize(Map<String, String> options) throws Exception; |
| | | |
| | | Importer startImport() throws Exception; |
| | | |
| | | void open() throws Exception; |
| | | |
| | | void openTree(TreeName name); |
| | | |
| | | <T> T read(ReadOperation<T> readTransaction) throws Exception; |
| | | |
| | | void update(WriteOperation updateTransaction) throws Exception; |
| | | |
| | | Cursor openCursor(TreeName name); |
| | | |
| | | @Override |
| | | void close(); |
| | | } |
| | | |
| | | @SuppressWarnings("serial") |
| | | static final class StorageRuntimeException extends RuntimeException |
| | | { |
| | | |
| | | public StorageRuntimeException(final String message) |
| | | { |
| | | super(message); |
| | | } |
| | | |
| | | public StorageRuntimeException(final String message, final Throwable cause) |
| | | { |
| | | super(message, cause); |
| | | } |
| | | |
| | | public StorageRuntimeException(final Throwable cause) |
| | | { |
| | | super(cause); |
| | | } |
| | | } |
| | | |
| | | /** Assumes name components don't contain a '/'. */ |
| | | static final class TreeName |
| | | { |
| | | public static TreeName of(final String... names) |
| | | { |
| | | return new TreeName(Arrays.asList(names)); |
| | | } |
| | | |
| | | private final List<String> names; |
| | | private final String s; |
| | | |
| | | public TreeName(final List<String> names) |
| | | { |
| | | this.names = names; |
| | | final StringBuilder builder = new StringBuilder(); |
| | | for (final String name : names) |
| | | { |
| | | builder.append('/'); |
| | | builder.append(name); |
| | | } |
| | | this.s = builder.toString(); |
| | | } |
| | | |
| | | public List<String> getNames() |
| | | { |
| | | return names; |
| | | } |
| | | |
| | | public TreeName child(final String name) |
| | | { |
| | | final List<String> newNames = new ArrayList<String>(names.size() + 1); |
| | | newNames.addAll(names); |
| | | newNames.add(name); |
| | | return new TreeName(newNames); |
| | | } |
| | | |
| | | public TreeName getSuffix() |
| | | { |
| | | if (names.size() == 0) |
| | | { |
| | | throw new IllegalStateException(); |
| | | } |
| | | return new TreeName(Collections.singletonList(names.get(0))); |
| | | } |
| | | |
| | | public boolean isSuffixOf(TreeName tree) |
| | | { |
| | | if (names.size() > tree.names.size()) |
| | | { |
| | | return false; |
| | | } |
| | | for (int i = 0; i < names.size(); i++) |
| | | { |
| | | if (!tree.names.get(i).equals(names.get(i))) |
| | | { |
| | | return false; |
| | | } |
| | | } |
| | | return true; |
| | | } |
| | | |
| | | @Override |
| | | public boolean equals(final Object obj) |
| | | { |
| | | if (this == obj) |
| | | { |
| | | return true; |
| | | } |
| | | else if (obj instanceof TreeName) |
| | | { |
| | | return s.equals(((TreeName) obj).s); |
| | | } |
| | | else |
| | | { |
| | | return false; |
| | | } |
| | | } |
| | | |
| | | @Override |
| | | public int hashCode() |
| | | { |
| | | return s.hashCode(); |
| | | } |
| | | |
| | | @Override |
| | | public String toString() |
| | | { |
| | | return s; |
| | | } |
| | | } |
| | | |
| | | interface WriteOperation |
| | | { |
| | | void run(WriteableStorage txn) throws Exception; |
| | | } |
| | | |
| | | interface WriteableStorage extends ReadableStorage |
| | | { |
| | | void put(TreeName name, ByteSequence key, ByteSequence value); |
| | | |
| | | boolean putIfAbsent(TreeName treeName, ByteSequence key, ByteSequence value); |
| | | |
| | | boolean remove(TreeName name, ByteSequence key); |
| | | |
| | | boolean remove(TreeName name, ByteSequence key, ByteSequence value); |
| | | } |
| | | |
| | | /** The configuration of this JE backend. */ |
| | | private LocalDBBackendCfg cfg; |
| | | /** The root JE container to use for this backend. */ |
| | | private RootContainer rootContainer; |
| | | /** A count of the total operation threads currently in the backend. */ |
| | | private final AtomicInteger threadTotalCount = new AtomicInteger(0); |
| | | /** A count of the write operation threads currently in the backend. */ |
| | | private final AtomicInteger threadWriteCount = new AtomicInteger(0); |
| | | /** The base DNs defined for this backend instance. */ |
| | | private DN[] baseDNs; |
| | | |
| | | private MonitorProvider<?> rootContainerMonitor; |
| | | private DiskSpaceMonitor diskMonitor; |
| | | |
| | | /** |
| | | * The controls supported by this backend. |
| | | */ |
| | | private static final Set<String> supportedControls = new HashSet<String>(Arrays.asList( |
| | | OID_SUBTREE_DELETE_CONTROL, |
| | | OID_PAGED_RESULTS_CONTROL, |
| | | OID_MANAGE_DSAIT_CONTROL, |
| | | OID_SERVER_SIDE_SORT_REQUEST_CONTROL, |
| | | OID_VLV_REQUEST_CONTROL)); |
| | | |
| | | /** Begin a Backend API method that reads the database. */ |
| | | private void readerBegin() |
| | | { |
| | | threadTotalCount.getAndIncrement(); |
| | | } |
| | | |
| | | /** End a Backend API method that reads the database. */ |
| | | private void readerEnd() |
| | | { |
| | | threadTotalCount.getAndDecrement(); |
| | | } |
| | | |
| | | /** Begin a Backend API method that writes the database. */ |
| | | private void writerBegin() |
| | | { |
| | | threadTotalCount.getAndIncrement(); |
| | | threadWriteCount.getAndIncrement(); |
| | | } |
| | | |
| | | /** End a Backend API method that writes the database. */ |
| | | private void writerEnd() |
| | | { |
| | | threadWriteCount.getAndDecrement(); |
| | | threadTotalCount.getAndDecrement(); |
| | | } |
| | | |
| | | |
| | | |
| | | /** |
| | | * Wait until there are no more threads accessing the database. It is assumed |
| | | * that new threads have been prevented from entering the database at the time |
| | | * this method is called. |
| | | */ |
| | | private void waitUntilQuiescent() |
| | | { |
| | | while (threadTotalCount.get() > 0) |
| | | { |
| | | // Still have threads in the database so sleep a little |
| | | try |
| | | { |
| | | Thread.sleep(500); |
| | | } |
| | | catch (InterruptedException e) |
| | | { |
| | | logger.traceException(e); |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * This method will attempt to checksum the current JE db environment by |
| | | * computing the Adler-32 checksum on the latest JE log file available. |
| | | * |
| | | * @return The checksum of JE db environment or zero if checksum failed. |
| | | */ |
| | | private long checksumDbEnv() { |
| | | |
| | | File parentDirectory = getFileForPath(cfg.getDBDirectory()); |
| | | File backendDirectory = new File(parentDirectory, cfg.getBackendId()); |
| | | |
| | | List<File> jdbFiles = new ArrayList<File>(); |
| | | if(backendDirectory.isDirectory()) |
| | | { |
| | | jdbFiles = |
| | | Arrays.asList(backendDirectory.listFiles(new FilenameFilter() { |
| | | @Override |
| | | public boolean accept(File dir, String name) { |
| | | return name.endsWith(".jdb"); |
| | | } |
| | | })); |
| | | } |
| | | |
| | | if ( !jdbFiles.isEmpty() ) { |
| | | Collections.sort(jdbFiles, Collections.reverseOrder()); |
| | | FileInputStream fis = null; |
| | | try { |
| | | fis = new FileInputStream(jdbFiles.get(0).toString()); |
| | | CheckedInputStream cis = new CheckedInputStream(fis, new Adler32()); |
| | | byte[] tempBuf = new byte[8192]; |
| | | while (cis.read(tempBuf) >= 0) { |
| | | } |
| | | |
| | | return cis.getChecksum().getValue(); |
| | | } catch (Exception e) { |
| | | logger.traceException(e); |
| | | } finally { |
| | | close(fis); |
| | | } |
| | | } |
| | | |
| | | return 0; |
| | | } |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public void configureBackend(LocalDBBackendCfg cfg) throws ConfigException |
| | | { |
| | | Reject.ifNull(cfg); |
| | | |
| | | this.cfg = cfg; |
| | | baseDNs = this.cfg.getBaseDN().toArray(new DN[0]); |
| | | } |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public void initializeBackend() |
| | | throws ConfigException, InitializationException |
| | | { |
| | | // Checksum this db environment and register its offline state id/checksum. |
| | | DirectoryServer.registerOfflineBackendStateID(getBackendID(), checksumDbEnv()); |
| | | |
| | | if (mustOpenRootContainer()) |
| | | { |
| | | rootContainer = initializeRootContainer(parseConfigEntry(cfg)); |
| | | } |
| | | |
| | | // Preload the database cache. |
| | | rootContainer.preload(cfg.getPreloadTimeLimit()); |
| | | |
| | | try |
| | | { |
| | | // Log an informational message about the number of entries. |
| | | logger.info(NOTE_JEB_BACKEND_STARTED, cfg.getBackendId(), rootContainer.getEntryCount()); |
| | | } |
| | | catch (StorageRuntimeException e) |
| | | { |
| | | logger.traceException(e); |
| | | LocalizableMessage message = WARN_JEB_GET_ENTRY_COUNT_FAILED.get(e.getMessage()); |
| | | throw new InitializationException(message, e); |
| | | } |
| | | |
| | | for (DN dn : cfg.getBaseDN()) |
| | | { |
| | | try |
| | | { |
| | | DirectoryServer.registerBaseDN(dn, this, false); |
| | | } |
| | | catch (Exception e) |
| | | { |
| | | logger.traceException(e); |
| | | throw new InitializationException(ERR_BACKEND_CANNOT_REGISTER_BASEDN.get(dn, e), e); |
| | | } |
| | | } |
| | | |
| | | // Register a monitor provider for the environment. |
| | | rootContainerMonitor = rootContainer.getMonitorProvider(); |
| | | DirectoryServer.registerMonitorProvider(rootContainerMonitor); |
| | | |
| | | // Register as disk space monitor handler |
| | | File parentDirectory = getFileForPath(cfg.getDBDirectory()); |
| | | File backendDirectory = |
| | | new File(parentDirectory, cfg.getBackendId()); |
| | | diskMonitor = new DiskSpaceMonitor(getBackendID() + " backend", |
| | | backendDirectory, cfg.getDiskLowThreshold(), cfg.getDiskFullThreshold(), |
| | | 5, TimeUnit.SECONDS, this); |
| | | diskMonitor.initializeMonitorProvider(null); |
| | | DirectoryServer.registerMonitorProvider(diskMonitor); |
| | | |
| | | //Register as an AlertGenerator. |
| | | DirectoryServer.registerAlertGenerator(this); |
| | | // Register this backend as a change listener. |
| | | cfg.addLocalDBChangeListener(this); |
| | | } |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public void finalizeBackend() |
| | | { |
| | | super.finalizeBackend(); |
| | | cfg.removeLocalDBChangeListener(this); |
| | | |
| | | // Deregister our base DNs. |
| | | for (DN dn : rootContainer.getBaseDNs()) |
| | | { |
| | | try |
| | | { |
| | | DirectoryServer.deregisterBaseDN(dn); |
| | | } |
| | | catch (Exception e) |
| | | { |
| | | logger.traceException(e); |
| | | } |
| | | } |
| | | |
| | | DirectoryServer.deregisterMonitorProvider(rootContainerMonitor); |
| | | DirectoryServer.deregisterMonitorProvider(diskMonitor); |
| | | |
| | | // We presume the server will prevent more operations coming into this |
| | | // backend, but there may be existing operations already in the |
| | | // backend. We need to wait for them to finish. |
| | | waitUntilQuiescent(); |
| | | |
| | | // Close the database. |
| | | try |
| | | { |
| | | rootContainer.close(); |
| | | rootContainer = null; |
| | | } |
| | | catch (StorageRuntimeException e) |
| | | { |
| | | logger.traceException(e); |
| | | logger.error(ERR_JEB_DATABASE_EXCEPTION, e.getMessage()); |
| | | } |
| | | |
| | | // Checksum this db environment and register its offline state id/checksum. |
| | | DirectoryServer.registerOfflineBackendStateID(getBackendID(), checksumDbEnv()); |
| | | DirectoryServer.deregisterAlertGenerator(this); |
| | | |
| | | // Make sure the thread counts are zero for next initialization. |
| | | threadTotalCount.set(0); |
| | | threadWriteCount.set(0); |
| | | |
| | | // Log an informational message. |
| | | logger.info(NOTE_BACKEND_OFFLINE, cfg.getBackendId()); |
| | | } |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public boolean isLocal() |
| | | { |
| | | return true; |
| | | } |
| | | |
| | | |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public boolean isIndexed(AttributeType attributeType, IndexType indexType) |
| | | { |
| | | try |
| | | { |
| | | EntryContainer ec = rootContainer.getEntryContainer(baseDNs[0]); |
| | | AttributeIndex ai = ec.getAttributeIndex(attributeType); |
| | | if (ai == null) |
| | | { |
| | | return false; |
| | | } |
| | | |
| | | Set<LocalDBIndexCfgDefn.IndexType> indexTypes = |
| | | ai.getConfiguration().getIndexType(); |
| | | switch (indexType) |
| | | { |
| | | case PRESENCE: |
| | | return indexTypes.contains(LocalDBIndexCfgDefn.IndexType.PRESENCE); |
| | | |
| | | case EQUALITY: |
| | | return indexTypes.contains(LocalDBIndexCfgDefn.IndexType.EQUALITY); |
| | | |
| | | case SUBSTRING: |
| | | case SUBINITIAL: |
| | | case SUBANY: |
| | | case SUBFINAL: |
| | | return indexTypes.contains(LocalDBIndexCfgDefn.IndexType.SUBSTRING); |
| | | |
| | | case GREATER_OR_EQUAL: |
| | | case LESS_OR_EQUAL: |
| | | return indexTypes.contains(LocalDBIndexCfgDefn.IndexType.ORDERING); |
| | | |
| | | case APPROXIMATE: |
| | | return indexTypes.contains(LocalDBIndexCfgDefn.IndexType.APPROXIMATE); |
| | | |
| | | default: |
| | | return false; |
| | | } |
| | | } |
| | | catch (Exception e) |
| | | { |
| | | logger.traceException(e); |
| | | |
| | | return false; |
| | | } |
| | | } |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public boolean supportsLDIFExport() |
| | | { |
| | | return true; |
| | | } |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public boolean supportsLDIFImport() |
| | | { |
| | | return true; |
| | | } |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public boolean supportsBackup() |
| | | { |
| | | return true; |
| | | } |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public boolean supportsBackup(BackupConfig backupConfig, |
| | | StringBuilder unsupportedReason) |
| | | { |
| | | return true; |
| | | } |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public boolean supportsRestore() |
| | | { |
| | | return true; |
| | | } |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public Set<String> getSupportedFeatures() |
| | | { |
| | | return Collections.emptySet(); |
| | | } |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public Set<String> getSupportedControls() |
| | | { |
| | | return supportedControls; |
| | | } |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public DN[] getBaseDNs() |
| | | { |
| | | return baseDNs; |
| | | } |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public long getEntryCount() |
| | | { |
| | | if (rootContainer != null) |
| | | { |
| | | try |
| | | { |
| | | return rootContainer.getEntryCount(); |
| | | } |
| | | catch (Exception e) |
| | | { |
| | | logger.traceException(e); |
| | | } |
| | | } |
| | | |
| | | return -1; |
| | | } |
| | | |
| | | |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public ConditionResult hasSubordinates(DN entryDN) |
| | | throws DirectoryException |
| | | { |
| | | long ret = numSubordinates(entryDN, false); |
| | | if(ret < 0) |
| | | { |
| | | return ConditionResult.UNDEFINED; |
| | | } |
| | | return ConditionResult.valueOf(ret != 0); |
| | | } |
| | | |
| | | |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public long numSubordinates(DN entryDN, boolean subtree) |
| | | throws DirectoryException |
| | | { |
| | | checkRootContainerInitialized(); |
| | | EntryContainer ec = rootContainer.getEntryContainer(entryDN); |
| | | if(ec == null) |
| | | { |
| | | return -1; |
| | | } |
| | | |
| | | readerBegin(); |
| | | ec.sharedLock.lock(); |
| | | try |
| | | { |
| | | long count = ec.getNumSubordinates(entryDN, subtree); |
| | | if(count == Long.MAX_VALUE) |
| | | { |
| | | // The index entry limit has exceeded and there is no count maintained. |
| | | return -1; |
| | | } |
| | | return count; |
| | | } |
| | | catch (StorageRuntimeException e) |
| | | { |
| | | logger.traceException(e); |
| | | throw createDirectoryException(e); |
| | | } |
| | | finally |
| | | { |
| | | ec.sharedLock.unlock(); |
| | | readerEnd(); |
| | | } |
| | | } |
| | | |
| | | |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public Entry getEntry(DN entryDN) throws DirectoryException |
| | | { |
| | | readerBegin(); |
| | | |
| | | checkRootContainerInitialized(); |
| | | EntryContainer ec = rootContainer.getEntryContainer(entryDN); |
| | | ec.sharedLock.lock(); |
| | | Entry entry; |
| | | try |
| | | { |
| | | entry = ec.getEntry(entryDN); |
| | | } |
| | | catch (StorageRuntimeException e) |
| | | { |
| | | logger.traceException(e); |
| | | throw createDirectoryException(e); |
| | | } |
| | | finally |
| | | { |
| | | ec.sharedLock.unlock(); |
| | | readerEnd(); |
| | | } |
| | | |
| | | return entry; |
| | | } |
| | | |
| | | |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public void addEntry(Entry entry, AddOperation addOperation) |
| | | throws DirectoryException, CanceledOperationException |
| | | { |
| | | checkDiskSpace(addOperation); |
| | | writerBegin(); |
| | | |
| | | checkRootContainerInitialized(); |
| | | EntryContainer ec = rootContainer.getEntryContainer(entry.getName()); |
| | | ec.sharedLock.lock(); |
| | | try |
| | | { |
| | | ec.addEntry(entry, addOperation); |
| | | } |
| | | catch (StorageRuntimeException e) |
| | | { |
| | | logger.traceException(e); |
| | | throw createDirectoryException(e); |
| | | } |
| | | finally |
| | | { |
| | | ec.sharedLock.unlock(); |
| | | writerEnd(); |
| | | } |
| | | } |
| | | |
| | | |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public void deleteEntry(DN entryDN, DeleteOperation deleteOperation) |
| | | throws DirectoryException, CanceledOperationException |
| | | { |
| | | checkDiskSpace(deleteOperation); |
| | | writerBegin(); |
| | | |
| | | checkRootContainerInitialized(); |
| | | EntryContainer ec = rootContainer.getEntryContainer(entryDN); |
| | | ec.sharedLock.lock(); |
| | | try |
| | | { |
| | | ec.deleteEntry(entryDN, deleteOperation); |
| | | } |
| | | catch (StorageRuntimeException e) |
| | | { |
| | | logger.traceException(e); |
| | | throw createDirectoryException(e); |
| | | } |
| | | finally |
| | | { |
| | | ec.sharedLock.unlock(); |
| | | writerEnd(); |
| | | } |
| | | } |
| | | |
| | | |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public void replaceEntry(Entry oldEntry, Entry newEntry, |
| | | ModifyOperation modifyOperation) throws DirectoryException, |
| | | CanceledOperationException |
| | | { |
| | | checkDiskSpace(modifyOperation); |
| | | writerBegin(); |
| | | |
| | | checkRootContainerInitialized(); |
| | | EntryContainer ec = rootContainer.getEntryContainer(newEntry.getName()); |
| | | ec.sharedLock.lock(); |
| | | |
| | | try |
| | | { |
| | | ec.replaceEntry(oldEntry, newEntry, modifyOperation); |
| | | } |
| | | catch (StorageRuntimeException e) |
| | | { |
| | | logger.traceException(e); |
| | | throw createDirectoryException(e); |
| | | } |
| | | finally |
| | | { |
| | | ec.sharedLock.unlock(); |
| | | writerEnd(); |
| | | } |
| | | } |
| | | |
| | | |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public void renameEntry(DN currentDN, Entry entry, |
| | | ModifyDNOperation modifyDNOperation) |
| | | throws DirectoryException, CanceledOperationException |
| | | { |
| | | checkDiskSpace(modifyDNOperation); |
| | | writerBegin(); |
| | | |
| | | checkRootContainerInitialized(); |
| | | EntryContainer currentContainer = rootContainer.getEntryContainer(currentDN); |
| | | EntryContainer container = rootContainer.getEntryContainer(entry.getName()); |
| | | |
| | | if (currentContainer != container) |
| | | { |
| | | // FIXME: No reason why we cannot implement a move between containers |
| | | // since the containers share the same database environment. |
| | | LocalizableMessage msg = WARN_JEB_FUNCTION_NOT_SUPPORTED.get(); |
| | | throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, msg); |
| | | } |
| | | |
| | | currentContainer.sharedLock.lock(); |
| | | try |
| | | { |
| | | currentContainer.renameEntry(currentDN, entry, modifyDNOperation); |
| | | } |
| | | catch (StorageRuntimeException e) |
| | | { |
| | | logger.traceException(e); |
| | | throw createDirectoryException(e); |
| | | } |
| | | finally |
| | | { |
| | | currentContainer.sharedLock.unlock(); |
| | | writerEnd(); |
| | | } |
| | | } |
| | | |
| | | |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public void search(SearchOperation searchOperation) |
| | | throws DirectoryException, CanceledOperationException |
| | | { |
| | | readerBegin(); |
| | | |
| | | checkRootContainerInitialized(); |
| | | EntryContainer ec = rootContainer.getEntryContainer(searchOperation.getBaseDN()); |
| | | ec.sharedLock.lock(); |
| | | |
| | | try |
| | | { |
| | | ec.search(searchOperation); |
| | | } |
| | | catch (StorageRuntimeException e) |
| | | { |
| | | logger.traceException(e); |
| | | throw createDirectoryException(e); |
| | | } |
| | | finally |
| | | { |
| | | ec.sharedLock.unlock(); |
| | | readerEnd(); |
| | | } |
| | | } |
| | | |
| | | private void checkRootContainerInitialized() throws DirectoryException |
| | | { |
| | | if (rootContainer == null) |
| | | { |
| | | LocalizableMessage msg = ERR_ROOT_CONTAINER_NOT_INITIALIZED.get(getBackendID()); |
| | | throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), msg); |
| | | } |
| | | } |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public void exportLDIF(LDIFExportConfig exportConfig) |
| | | throws DirectoryException |
| | | { |
| | | // If the backend already has the root container open, we must use the same |
| | | // underlying root container |
| | | boolean openRootContainer = mustOpenRootContainer(); |
| | | final ResultCode errorRC = DirectoryServer.getServerErrorResultCode(); |
| | | try |
| | | { |
| | | if (openRootContainer) |
| | | { |
| | | rootContainer = getReadOnlyRootContainer(); |
| | | } |
| | | |
| | | ExportJob exportJob = new ExportJob(exportConfig); |
| | | exportJob.exportLDIF(rootContainer); |
| | | } |
| | | catch (IOException ioe) |
| | | { |
| | | logger.traceException(ioe); |
| | | throw new DirectoryException(errorRC, ERR_JEB_EXPORT_IO_ERROR.get(ioe.getMessage())); |
| | | } |
| | | catch (StorageRuntimeException de) |
| | | { |
| | | logger.traceException(de); |
| | | throw createDirectoryException(de); |
| | | } |
| | | catch (ConfigException ce) |
| | | { |
| | | throw new DirectoryException(errorRC, ce.getMessageObject()); |
| | | } |
| | | catch (IdentifiedException e) |
| | | { |
| | | if (e instanceof DirectoryException) |
| | | { |
| | | throw (DirectoryException) e; |
| | | } |
| | | logger.traceException(e); |
| | | throw new DirectoryException(errorRC, e.getMessageObject()); |
| | | } |
| | | finally |
| | | { |
| | | closeTemporaryRootContainer(openRootContainer); |
| | | } |
| | | } |
| | | |
| | | private boolean mustOpenRootContainer() |
| | | { |
| | | return rootContainer == null; |
| | | } |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public LDIFImportResult importLDIF(LDIFImportConfig importConfig) |
| | | throws DirectoryException |
| | | { |
| | | RuntimeInformation.logInfo(); |
| | | |
| | | // If the backend already has the root container open, we must use the same |
| | | // underlying root container |
| | | boolean openRootContainer = rootContainer == null; |
| | | |
| | | // If the rootContainer is open, the backend is initialized by something else. |
| | | // We can't do import while the backend is online. |
| | | final ResultCode errorRC = DirectoryServer.getServerErrorResultCode(); |
| | | if(!openRootContainer) |
| | | { |
| | | throw new DirectoryException(errorRC, ERR_JEB_IMPORT_BACKEND_ONLINE.get()); |
| | | } |
| | | |
| | | try |
| | | { |
| | | final EnvironmentConfig envConfig = getEnvConfigForImport(); |
| | | |
| | | if (!importConfig.appendToExistingData() |
| | | && (importConfig.clearBackend() || cfg.getBaseDN().size() <= 1)) |
| | | { |
| | | // We have the writer lock on the environment, now delete the |
| | | // environment and re-open it. Only do this when we are |
| | | // importing to all the base DNs in the backend or if the backend only |
| | | // have one base DN. |
| | | File parentDirectory = getFileForPath(cfg.getDBDirectory()); |
| | | File backendDirectory = new File(parentDirectory, cfg.getBackendId()); |
| | | // If the backend does not exist the import will create it. |
| | | if (backendDirectory.exists()) |
| | | { |
| | | EnvManager.removeFiles(backendDirectory.getPath()); |
| | | } |
| | | } |
| | | |
| | | throw new NotImplementedException(); |
| | | // Importer importer = new Importer(importConfig, cfg, envConfig); |
| | | // rootContainer = initializeRootContainer(envConfig); |
| | | // return importer.processImport(rootContainer); |
| | | } |
| | | catch (ExecutionException execEx) |
| | | { |
| | | logger.traceException(execEx); |
| | | if (execEx.getCause() instanceof DirectoryException) |
| | | { |
| | | throw ((DirectoryException) execEx.getCause()); |
| | | } |
| | | throw new DirectoryException(errorRC, ERR_EXECUTION_ERROR.get(execEx.getMessage())); |
| | | } |
| | | catch (InterruptedException intEx) |
| | | { |
| | | logger.traceException(intEx); |
| | | throw new DirectoryException(errorRC, ERR_INTERRUPTED_ERROR.get(intEx.getMessage())); |
| | | } |
| | | catch (JebException je) |
| | | { |
| | | logger.traceException(je); |
| | | throw new DirectoryException(errorRC, je.getMessageObject()); |
| | | } |
| | | catch (InitializationException ie) |
| | | { |
| | | logger.traceException(ie); |
| | | throw new DirectoryException(errorRC, ie.getMessageObject()); |
| | | } |
| | | catch (ConfigException ce) |
| | | { |
| | | logger.traceException(ce); |
| | | throw new DirectoryException(errorRC, ce.getMessageObject()); |
| | | } |
| | | finally |
| | | { |
| | | // leave the backend in the same state. |
| | | try |
| | | { |
| | | if (rootContainer != null) |
| | | { |
| | | long startTime = System.currentTimeMillis(); |
| | | rootContainer.close(); |
| | | long finishTime = System.currentTimeMillis(); |
| | | long closeTime = (finishTime - startTime) / 1000; |
| | | logger.info(NOTE_JEB_IMPORT_LDIF_ROOTCONTAINER_CLOSE, closeTime); |
| | | rootContainer = null; |
| | | } |
| | | |
| | | // Sync the environment to disk. |
| | | logger.info(NOTE_JEB_IMPORT_CLOSING_DATABASE); |
| | | } |
| | | catch (StorageRuntimeException de) |
| | | { |
| | | logger.traceException(de); |
| | | } |
| | | } |
| | | } |
| | | |
| | | private EnvironmentConfig getEnvConfigForImport() |
| | | { |
| | | final EnvironmentConfig envConfig = new EnvironmentConfig(); |
| | | envConfig.setAllowCreate(true); |
| | | envConfig.setTransactional(false); |
| | | envConfig.setDurability(Durability.COMMIT_NO_SYNC); |
| | | envConfig.setLockTimeout(0, TimeUnit.SECONDS); |
| | | envConfig.setTxnTimeout(0, TimeUnit.SECONDS); |
| | | envConfig.setConfigParam(CLEANER_MIN_FILE_UTILIZATION, |
| | | String.valueOf(cfg.getDBCleanerMinUtilization())); |
| | | envConfig.setConfigParam(LOG_FILE_MAX, |
| | | String.valueOf(cfg.getDBLogFileMax())); |
| | | return envConfig; |
| | | } |
| | | |
| | | /** |
| | | * Verify the integrity of the backend instance. |
| | | * @param verifyConfig The verify configuration. |
| | | * @param statEntry Optional entry to save stats into. |
| | | * @return The error count. |
| | | * @throws ConfigException If an unrecoverable problem arises during |
| | | * initialization. |
| | | * @throws InitializationException If a problem occurs during initialization |
| | | * that is not related to the server |
| | | * configuration. |
| | | * @throws DirectoryException If a Directory Server error occurs. |
| | | */ |
| | | public long verifyBackend(VerifyConfig verifyConfig, Entry statEntry) |
| | | throws InitializationException, ConfigException, DirectoryException |
| | | { |
| | | // If the backend already has the root container open, we must use the same |
| | | // underlying root container |
| | | final boolean openRootContainer = mustOpenRootContainer(); |
| | | try |
| | | { |
| | | if (openRootContainer) |
| | | { |
| | | rootContainer = getReadOnlyRootContainer(); |
| | | } |
| | | |
| | | VerifyJob verifyJob = new VerifyJob(verifyConfig); |
| | | return verifyJob.verifyBackend(rootContainer, statEntry); |
| | | } |
| | | catch (StorageRuntimeException e) |
| | | { |
| | | logger.traceException(e); |
| | | throw createDirectoryException(e); |
| | | } |
| | | catch (JebException e) |
| | | { |
| | | logger.traceException(e); |
| | | throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), |
| | | e.getMessageObject()); |
| | | } |
| | | finally |
| | | { |
| | | closeTemporaryRootContainer(openRootContainer); |
| | | } |
| | | } |
| | | |
| | | |
| | | /** |
| | | * Rebuild index(es) in the backend instance. Note that the server will not |
| | | * explicitly initialize this backend before calling this method. |
| | | * @param rebuildConfig The rebuild configuration. |
| | | * @throws ConfigException If an unrecoverable problem arises during |
| | | * initialization. |
| | | * @throws InitializationException If a problem occurs during initialization |
| | | * that is not related to the server |
| | | * configuration. |
| | | * @throws DirectoryException If a Directory Server error occurs. |
| | | */ |
| | | public void rebuildBackend(RebuildConfig rebuildConfig) |
| | | throws InitializationException, ConfigException, DirectoryException |
| | | { |
| | | // If the backend already has the root container open, we must use the same |
| | | // underlying root container |
| | | boolean openRootContainer = mustOpenRootContainer(); |
| | | |
| | | /* |
| | | * If the rootContainer is open, the backend is initialized by something |
| | | * else. We can't do any rebuild of system indexes while others are using |
| | | * this backend. |
| | | */ |
| | | final ResultCode errorRC = DirectoryServer.getServerErrorResultCode(); |
| | | if(!openRootContainer && rebuildConfig.includesSystemIndex()) |
| | | { |
| | | throw new DirectoryException(errorRC, ERR_JEB_REBUILD_BACKEND_ONLINE.get()); |
| | | } |
| | | |
| | | try |
| | | { |
| | | final EnvironmentConfig envConfig; |
| | | if (openRootContainer) |
| | | { |
| | | envConfig = getEnvConfigForImport(); |
| | | rootContainer = initializeRootContainer(envConfig); |
| | | } |
| | | else |
| | | { |
| | | envConfig = parseConfigEntry(cfg); |
| | | |
| | | } |
| | | throw new NotImplementedException(); |
| | | // final Importer importer = new Importer(rebuildConfig, cfg, envConfig); |
| | | // importer.rebuildIndexes(rootContainer); |
| | | } |
| | | catch (ExecutionException execEx) |
| | | { |
| | | logger.traceException(execEx); |
| | | throw new DirectoryException(errorRC, ERR_EXECUTION_ERROR.get(execEx.getMessage())); |
| | | } |
| | | catch (InterruptedException intEx) |
| | | { |
| | | logger.traceException(intEx); |
| | | throw new DirectoryException(errorRC, ERR_INTERRUPTED_ERROR.get(intEx.getMessage())); |
| | | } |
| | | catch (ConfigException ce) |
| | | { |
| | | logger.traceException(ce); |
| | | throw new DirectoryException(errorRC, ce.getMessageObject()); |
| | | } |
| | | catch (JebException e) |
| | | { |
| | | logger.traceException(e); |
| | | throw new DirectoryException(errorRC, e.getMessageObject()); |
| | | } |
| | | catch (InitializationException e) |
| | | { |
| | | logger.traceException(e); |
| | | throw new InitializationException(e.getMessageObject()); |
| | | } |
| | | finally |
| | | { |
| | | closeTemporaryRootContainer(openRootContainer); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * If a root container was opened in the calling method method as read only, |
| | | * close it to leave the backend in the same state. |
| | | */ |
| | | private void closeTemporaryRootContainer(boolean openRootContainer) |
| | | { |
| | | if (openRootContainer && rootContainer != null) |
| | | { |
| | | try |
| | | { |
| | | rootContainer.close(); |
| | | rootContainer = null; |
| | | } |
| | | catch (StorageRuntimeException e) |
| | | { |
| | | logger.traceException(e); |
| | | } |
| | | } |
| | | } |
| | | |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public void createBackup(BackupConfig backupConfig) throws DirectoryException |
| | | { |
| | | BackupManager backupManager = new BackupManager(getBackendID()); |
| | | File parentDir = getFileForPath(cfg.getDBDirectory()); |
| | | File backendDir = new File(parentDir, cfg.getBackendId()); |
| | | backupManager.createBackup(backendDir, backupConfig); |
| | | } |
| | | |
| | | |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public void removeBackup(BackupDirectory backupDirectory, String backupID) |
| | | throws DirectoryException |
| | | { |
| | | BackupManager backupManager = new BackupManager(getBackendID()); |
| | | backupManager.removeBackup(backupDirectory, backupID); |
| | | } |
| | | |
| | | |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public void restoreBackup(RestoreConfig restoreConfig) |
| | | throws DirectoryException |
| | | { |
| | | BackupManager backupManager = new BackupManager(getBackendID()); |
| | | File parentDir = getFileForPath(cfg.getDBDirectory()); |
| | | File backendDir = new File(parentDir, cfg.getBackendId()); |
| | | backupManager.restoreBackup(backendDir, restoreConfig); |
| | | } |
| | | |
| | | |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public boolean isConfigurationAcceptable(LocalDBBackendCfg config, |
| | | List<LocalizableMessage> unacceptableReasons) |
| | | { |
| | | return isConfigurationChangeAcceptable(config, unacceptableReasons); |
| | | } |
| | | |
| | | |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public boolean isConfigurationChangeAcceptable( |
| | | LocalDBBackendCfg cfg, |
| | | List<LocalizableMessage> unacceptableReasons) |
| | | { |
| | | // Make sure that the logging level value is acceptable. |
| | | try { |
| | | Level.parse(cfg.getDBLoggingLevel()); |
| | | return true; |
| | | } catch (Exception e) { |
| | | unacceptableReasons.add(ERR_JEB_INVALID_LOGGING_LEVEL.get(cfg.getDBLoggingLevel(), cfg.dn())); |
| | | return false; |
| | | } |
| | | } |
| | | |
| | | |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public ConfigChangeResult applyConfigurationChange(LocalDBBackendCfg newCfg) |
| | | { |
| | | ResultCode resultCode = ResultCode.SUCCESS; |
| | | ArrayList<LocalizableMessage> messages = new ArrayList<LocalizableMessage>(); |
| | | |
| | | try |
| | | { |
| | | if(rootContainer != null) |
| | | { |
| | | SortedSet<DN> newBaseDNs = newCfg.getBaseDN(); |
| | | DN[] newBaseDNsArray = newBaseDNs.toArray(new DN[newBaseDNs.size()]); |
| | | |
| | | // Check for changes to the base DNs. |
| | | removeDeletedBaseDNs(newBaseDNs); |
| | | ConfigChangeResult failure = createNewBaseDNs(newBaseDNsArray, messages); |
| | | if (failure != null) |
| | | { |
| | | return failure; |
| | | } |
| | | |
| | | baseDNs = newBaseDNsArray; |
| | | } |
| | | |
| | | if(cfg.getDiskFullThreshold() != newCfg.getDiskFullThreshold() || |
| | | cfg.getDiskLowThreshold() != newCfg.getDiskLowThreshold()) |
| | | { |
| | | diskMonitor.setFullThreshold(newCfg.getDiskFullThreshold()); |
| | | diskMonitor.setLowThreshold(newCfg.getDiskLowThreshold()); |
| | | } |
| | | |
| | | // Put the new configuration in place. |
| | | this.cfg = newCfg; |
| | | } |
| | | catch (Exception e) |
| | | { |
| | | messages.add(LocalizableMessage.raw(stackTraceToSingleLineString(e))); |
| | | return new ConfigChangeResult( |
| | | DirectoryServer.getServerErrorResultCode(), false, messages); |
| | | } |
| | | |
| | | return new ConfigChangeResult(resultCode, false, messages); |
| | | } |
| | | |
| | | private void removeDeletedBaseDNs(SortedSet<DN> newBaseDNs) throws DirectoryException |
| | | { |
| | | for (DN baseDN : cfg.getBaseDN()) |
| | | { |
| | | if (!newBaseDNs.contains(baseDN)) |
| | | { |
| | | // The base DN was deleted. |
| | | DirectoryServer.deregisterBaseDN(baseDN); |
| | | EntryContainer ec = rootContainer.unregisterEntryContainer(baseDN); |
| | | ec.close(); |
| | | ec.delete(); |
| | | } |
| | | } |
| | | } |
| | | |
| | | private ConfigChangeResult createNewBaseDNs(DN[] newBaseDNsArray, ArrayList<LocalizableMessage> messages) |
| | | { |
| | | for (DN baseDN : newBaseDNsArray) |
| | | { |
| | | if (!rootContainer.getBaseDNs().contains(baseDN)) |
| | | { |
| | | try |
| | | { |
| | | // The base DN was added. |
| | | EntryContainer ec = rootContainer.openEntryContainer(baseDN, null); |
| | | rootContainer.registerEntryContainer(baseDN, ec); |
| | | DirectoryServer.registerBaseDN(baseDN, this, false); |
| | | } |
| | | catch (Exception e) |
| | | { |
| | | logger.traceException(e); |
| | | |
| | | ResultCode resultCode = DirectoryServer.getServerErrorResultCode(); |
| | | messages.add(ERR_BACKEND_CANNOT_REGISTER_BASEDN.get(baseDN, e)); |
| | | return new ConfigChangeResult(resultCode, false, messages); |
| | | } |
| | | } |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | /** |
| | | * Returns a handle to the JE root container currently used by this backend. |
| | | * The rootContainer could be NULL if the backend is not initialized. |
| | | * |
| | | * @return The RootContainer object currently used by this backend. |
| | | */ |
| | | public RootContainer getRootContainer() |
| | | { |
| | | return rootContainer; |
| | | } |
| | | |
| | | /** |
| | | * Returns a new read-only handle to the JE root container for this backend. |
| | | * The caller is responsible for closing the root container after use. |
| | | * |
| | | * @return The read-only RootContainer object for this backend. |
| | | * |
| | | * @throws ConfigException If an unrecoverable problem arises during |
| | | * initialization. |
| | | * @throws InitializationException If a problem occurs during initialization |
| | | * that is not related to the server |
| | | * configuration. |
| | | */ |
| | | public RootContainer getReadOnlyRootContainer() |
| | | throws ConfigException, InitializationException |
| | | { |
| | | EnvironmentConfig envConfig = parseConfigEntry(cfg); |
| | | |
| | | envConfig.setReadOnly(true); |
| | | envConfig.setAllowCreate(false); |
| | | envConfig.setTransactional(false); |
| | | envConfig.setConfigParam(ENV_IS_LOCKING, "true"); |
| | | envConfig.setConfigParam(ENV_RUN_CHECKPOINTER, "true"); |
| | | |
| | | return initializeRootContainer(envConfig); |
| | | } |
| | | |
| | | /** |
| | | * Clears all the entries from the backend. This method is for test cases |
| | | * that use the JE backend. |
| | | * |
| | | * @throws ConfigException If an unrecoverable problem arises in the |
| | | * process of performing the initialization. |
| | | * |
| | | * @throws JebException If an error occurs while removing the data. |
| | | */ |
| | | public void clearBackend() |
| | | throws ConfigException, JebException |
| | | { |
| | | // Determine the backend database directory. |
| | | File parentDirectory = getFileForPath(cfg.getDBDirectory()); |
| | | File backendDirectory = new File(parentDirectory, cfg.getBackendId()); |
| | | EnvManager.removeFiles(backendDirectory.getPath()); |
| | | } |
| | | |
| | | /** |
| | | * Creates a customized DirectoryException from the StorageRuntimeException |
| | | * thrown by JE backend. |
| | | * |
| | | * @param e |
| | | * The StorageRuntimeException to be converted. |
| | | * @return DirectoryException created from exception. |
| | | */ |
| | | DirectoryException createDirectoryException(StorageRuntimeException e) |
| | | { |
| | | if (true) { |
| | | throw new NotImplementedException(); |
| | | } |
| | | if (/*e instanceof EnvironmentFailureException && */ !rootContainer.isValid()) { |
| | | LocalizableMessage message = NOTE_BACKEND_ENVIRONMENT_UNUSABLE.get(getBackendID()); |
| | | logger.info(message); |
| | | DirectoryServer.sendAlertNotification(DirectoryServer.getInstance(), |
| | | ALERT_TYPE_BACKEND_ENVIRONMENT_UNUSABLE, message); |
| | | } |
| | | |
| | | String jeMessage = e.getMessage(); |
| | | if (jeMessage == null) { |
| | | jeMessage = stackTraceToSingleLineString(e); |
| | | } |
| | | LocalizableMessage message = ERR_JEB_DATABASE_EXCEPTION.get(jeMessage); |
| | | return new DirectoryException( |
| | | DirectoryServer.getServerErrorResultCode(), message, e); |
| | | } |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public String getClassName() { |
| | | return BackendImpl.class.getName(); |
| | | } |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public Map<String, String> getAlerts() |
| | | { |
| | | Map<String, String> alerts = new LinkedHashMap<String, String>(); |
| | | |
| | | alerts.put(ALERT_TYPE_BACKEND_ENVIRONMENT_UNUSABLE, |
| | | ALERT_DESCRIPTION_BACKEND_ENVIRONMENT_UNUSABLE); |
| | | alerts.put(ALERT_TYPE_DISK_SPACE_LOW, |
| | | ALERT_DESCRIPTION_DISK_SPACE_LOW); |
| | | alerts.put(ALERT_TYPE_DISK_FULL, |
| | | ALERT_DESCRIPTION_DISK_FULL); |
| | | return alerts; |
| | | } |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public DN getComponentEntryDN() { |
| | | return cfg.dn(); |
| | | } |
| | | |
| | | private RootContainer initializeRootContainer(EnvironmentConfig envConfig) |
| | | throws ConfigException, InitializationException { |
| | | // Open the database environment |
| | | try { |
| | | RootContainer rc = new RootContainer(this, cfg); |
| | | rc.open(envConfig); |
| | | return rc; |
| | | } |
| | | catch (StorageRuntimeException e) |
| | | { |
| | | logger.traceException(e); |
| | | LocalizableMessage message = ERR_JEB_OPEN_ENV_FAIL.get(e.getMessage()); |
| | | throw new InitializationException(message, e); |
| | | } |
| | | } |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public void preloadEntryCache() throws |
| | | UnsupportedOperationException { |
| | | EntryCachePreloader preloader = new EntryCachePreloader(this); |
| | | preloader.preload(); |
| | | } |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public void diskLowThresholdReached(DiskSpaceMonitor monitor) { |
| | | LocalizableMessage msg = ERR_JEB_DISK_LOW_THRESHOLD_REACHED.get( |
| | | monitor.getDirectory().getPath(), cfg.getBackendId(), monitor.getFreeSpace(), |
| | | Math.max(monitor.getLowThreshold(), monitor.getFullThreshold())); |
| | | DirectoryServer.sendAlertNotification(this, ALERT_TYPE_DISK_SPACE_LOW, msg); |
| | | } |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public void diskFullThresholdReached(DiskSpaceMonitor monitor) { |
| | | LocalizableMessage msg = ERR_JEB_DISK_FULL_THRESHOLD_REACHED.get( |
| | | monitor.getDirectory().getPath(), cfg.getBackendId(), monitor.getFreeSpace(), |
| | | Math.max(monitor.getLowThreshold(), monitor.getFullThreshold())); |
| | | DirectoryServer.sendAlertNotification(this, ALERT_TYPE_DISK_FULL, msg); |
| | | } |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public void diskSpaceRestored(DiskSpaceMonitor monitor) { |
| | | logger.error(NOTE_JEB_DISK_SPACE_RESTORED, monitor.getFreeSpace(), |
| | | monitor.getDirectory().getPath(), cfg.getBackendId(), |
| | | Math.max(monitor.getLowThreshold(), monitor.getFullThreshold())); |
| | | } |
| | | |
| | | private void checkDiskSpace(Operation operation) throws DirectoryException |
| | | { |
| | | if(diskMonitor.isFullThresholdReached() || |
| | | (diskMonitor.isLowThresholdReached() |
| | | && operation != null |
| | | && !operation.getClientConnection().hasPrivilege( |
| | | Privilege.BYPASS_LOCKDOWN, operation))) |
| | | { |
| | | throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, |
| | | WARN_JEB_OUT_OF_DISK_SPACE.get()); |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | /* |
| | | * CDDL HEADER START |
| | | * |
| | | * The contents of this file are subject to the terms of the |
| | | * Common Development and Distribution License, Version 1.0 only |
| | | * (the "License"). You may not use this file except in compliance |
| | | * with the License. |
| | | * |
| | | * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt |
| | | * or http://forgerock.org/license/CDDLv1.0.html. |
| | | * See the License for the specific language governing permissions |
| | | * and limitations under the License. |
| | | * |
| | | * When distributing Covered Code, include this CDDL HEADER in each |
| | | * file and include the License file at legal-notices/CDDLv1_0.txt. |
| | | * If applicable, add the following below this CDDL HEADER, with the |
| | | * fields enclosed by brackets "[]" replaced with your own identifying |
| | | * information: |
| | | * Portions Copyright [yyyy] [name of copyright owner] |
| | | * |
| | | * CDDL HEADER END |
| | | * |
| | | * |
| | | * Copyright 2006-2009 Sun Microsystems, Inc. |
| | | * Portions Copyright 2013-2014 ForgeRock AS. |
| | | */ |
| | | package org.opends.server.backends.pluggable; |
| | | import org.forgerock.i18n.LocalizableMessage; |
| | | import org.forgerock.opendj.config.server.ConfigException; |
| | | import org.opends.server.core.DirectoryServer; |
| | | import org.opends.server.util.DynamicConstants; |
| | | import org.opends.server.util.StaticUtils; |
| | | import org.opends.server.types.CryptoManagerException; |
| | | |
| | | import javax.crypto.Mac; |
| | | import java.io.BufferedReader; |
| | | import java.io.File; |
| | | import java.io.FileInputStream; |
| | | import java.io.FileNotFoundException; |
| | | import java.io.FileOutputStream; |
| | | import java.io.FilenameFilter; |
| | | import java.io.InputStream; |
| | | import java.io.InputStreamReader; |
| | | import java.io.IOException; |
| | | import java.io.OutputStream; |
| | | import java.io.OutputStreamWriter; |
| | | import java.io.Writer; |
| | | import java.security.MessageDigest; |
| | | import java.util.*; |
| | | import java.util.zip.Deflater; |
| | | import java.util.zip.ZipEntry; |
| | | import java.util.zip.ZipInputStream; |
| | | import java.util.zip.ZipOutputStream; |
| | | |
| | | import org.opends.server.types.*; |
| | | import org.forgerock.i18n.slf4j.LocalizedLogger; |
| | | import static org.opends.messages.JebMessages.*; |
| | | import static org.opends.server.util.ServerConstants.*; |
| | | import static org.opends.server.util.StaticUtils.*; |
| | | |
| | | /** |
| | | * A backup manager for JE backends. |
| | | */ |
| | | public class BackupManager |
| | | { |
| | | private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); |
| | | |
| | | /** |
| | | * The common prefix for archive files. |
| | | */ |
| | | public static final String BACKUP_BASE_FILENAME = "backup-"; |
| | | |
| | | /** |
| | | * The name of the property that holds the name of the latest log file |
| | | * at the time the backup was created. |
| | | */ |
| | | public static final String PROPERTY_LAST_LOGFILE_NAME = "last_logfile_name"; |
| | | |
| | | /** |
| | | * The name of the property that holds the size of the latest log file |
| | | * at the time the backup was created. |
| | | */ |
| | | public static final String PROPERTY_LAST_LOGFILE_SIZE = "last_logfile_size"; |
| | | |
| | | |
| | | /** |
| | | * The name of the entry in an incremental backup archive file |
| | | * containing a list of log files that are unchanged since the |
| | | * previous backup. |
| | | */ |
| | | public static final String ZIPENTRY_UNCHANGED_LOGFILES = "unchanged.txt"; |
| | | |
| | | /** |
| | | * The name of a dummy entry in the backup archive file that will act |
| | | * as a placeholder in case a backup is done on an empty backend. |
| | | */ |
| | | public static final String ZIPENTRY_EMPTY_PLACEHOLDER = "empty.placeholder"; |
| | | |
| | | |
| | | /** |
| | | * The backend ID. |
| | | */ |
| | | private String backendID; |
| | | |
| | | |
| | | /** |
| | | * Construct a backup manager for a JE backend. |
| | | * @param backendID The ID of the backend instance for which a backup |
| | | * manager is required. |
| | | */ |
| | | public BackupManager(String backendID) |
| | | { |
| | | this.backendID = backendID; |
| | | } |
| | | |
| | | /** |
| | | * Create a backup of the JE backend. The backup is stored in a single zip |
| | | * file in the backup directory. If the backup is incremental, then the |
| | | * first entry in the zip is a text file containing a list of all the JE |
| | | * log files that are unchanged since the previous backup. The remaining |
| | | * zip entries are the JE log files themselves, which, for an incremental, |
| | | * only include those files that have changed. |
| | | * @param backendDir The directory of the backend instance for |
| | | * which the backup is required. |
| | | * @param backupConfig The configuration to use when performing the backup. |
| | | * @throws DirectoryException If a Directory Server error occurs. |
| | | */ |
| | | public void createBackup(File backendDir, BackupConfig backupConfig) |
| | | throws DirectoryException |
| | | { |
| | | // Get the properties to use for the backup. |
| | | String backupID = backupConfig.getBackupID(); |
| | | BackupDirectory backupDir = backupConfig.getBackupDirectory(); |
| | | boolean incremental = backupConfig.isIncremental(); |
| | | String incrBaseID = backupConfig.getIncrementalBaseID(); |
| | | boolean compress = backupConfig.compressData(); |
| | | boolean encrypt = backupConfig.encryptData(); |
| | | boolean hash = backupConfig.hashData(); |
| | | boolean signHash = backupConfig.signHash(); |
| | | |
| | | |
| | | HashMap<String,String> backupProperties = new HashMap<String,String>(); |
| | | |
| | | // Get the crypto manager and use it to obtain references to the message |
| | | // digest and/or MAC to use for hashing and/or signing. |
| | | CryptoManager cryptoManager = DirectoryServer.getCryptoManager(); |
| | | Mac mac = null; |
| | | MessageDigest digest = null; |
| | | String macKeyID = null; |
| | | |
| | | if (hash) |
| | | { |
| | | if (signHash) |
| | | { |
| | | try |
| | | { |
| | | macKeyID = cryptoManager.getMacEngineKeyEntryID(); |
| | | backupProperties.put(BACKUP_PROPERTY_MAC_KEY_ID, macKeyID); |
| | | |
| | | mac = cryptoManager.getMacEngine(macKeyID); |
| | | } |
| | | catch (Exception e) |
| | | { |
| | | logger.traceException(e); |
| | | |
| | | LocalizableMessage message = ERR_JEB_BACKUP_CANNOT_GET_MAC.get( |
| | | macKeyID, stackTraceToSingleLineString(e)); |
| | | throw new DirectoryException( |
| | | DirectoryServer.getServerErrorResultCode(), message, e); |
| | | } |
| | | } |
| | | else |
| | | { |
| | | String digestAlgorithm = cryptoManager |
| | | .getPreferredMessageDigestAlgorithm(); |
| | | backupProperties.put(BACKUP_PROPERTY_DIGEST_ALGORITHM, digestAlgorithm); |
| | | |
| | | try |
| | | { |
| | | digest = cryptoManager.getPreferredMessageDigest(); |
| | | } |
| | | catch (Exception e) |
| | | { |
| | | logger.traceException(e); |
| | | |
| | | LocalizableMessage message = ERR_JEB_BACKUP_CANNOT_GET_DIGEST.get( |
| | | digestAlgorithm, stackTraceToSingleLineString(e)); |
| | | throw new DirectoryException( |
| | | DirectoryServer.getServerErrorResultCode(), message, e); |
| | | } |
| | | } |
| | | } |
| | | |
| | | |
| | | // Date the backup. |
| | | Date backupDate = new Date(); |
| | | |
| | | // If this is an incremental, determine the base backup for this backup. |
| | | HashSet<String> dependencies = new HashSet<String>(); |
| | | BackupInfo baseBackup = null; |
| | | |
| | | if (incremental) |
| | | { |
| | | if (incrBaseID == null) |
| | | { |
| | | // The default is to use the latest backup as base. |
| | | if (backupDir.getLatestBackup() != null) |
| | | { |
| | | incrBaseID = backupDir.getLatestBackup().getBackupID(); |
| | | } |
| | | } |
| | | |
| | | if (incrBaseID == null) |
| | | { |
| | | // No incremental backup ID: log a message informing that a backup |
| | | // could not be found and that a normal backup will be done. |
| | | incremental = false; |
| | | logger.warn(WARN_BACKUPDB_INCREMENTAL_NOT_FOUND_DOING_NORMAL, backupDir.getPath()); |
| | | } |
| | | else |
| | | { |
| | | baseBackup = getBackupInfo(backupDir, incrBaseID); |
| | | } |
| | | } |
| | | |
| | | // Get information about the latest log file from the base backup. |
| | | String latestFileName = null; |
| | | long latestFileSize = 0; |
| | | if (baseBackup != null) |
| | | { |
| | | HashMap<String,String> properties = baseBackup.getBackupProperties(); |
| | | latestFileName = properties.get(PROPERTY_LAST_LOGFILE_NAME); |
| | | latestFileSize = Long.parseLong( |
| | | properties.get(PROPERTY_LAST_LOGFILE_SIZE)); |
| | | } |
| | | |
| | | /* |
| | | Create an output stream that will be used to write the archive file. At |
| | | its core, it will be a file output stream to put a file on the disk. If |
| | | we are to encrypt the data, then that file output stream will be wrapped |
| | | in a cipher output stream. The resulting output stream will then be |
| | | wrapped by a zip output stream (which may or may not actually use |
| | | compression). |
| | | */ |
| | | String archiveFilename = null; |
| | | OutputStream outputStream; |
| | | File archiveFile; |
| | | try |
| | | { |
| | | archiveFilename = BACKUP_BASE_FILENAME + backendID + "-" + backupID; |
| | | archiveFile = new File(backupDir.getPath(), archiveFilename); |
| | | if (archiveFile.exists()) |
| | | { |
| | | int i=1; |
| | | while (true) |
| | | { |
| | | archiveFile = new File(backupDir.getPath(), |
| | | archiveFilename + "." + i); |
| | | if (archiveFile.exists()) |
| | | { |
| | | i++; |
| | | } |
| | | else |
| | | { |
| | | archiveFilename = archiveFilename + "." + i; |
| | | break; |
| | | } |
| | | } |
| | | } |
| | | |
| | | outputStream = new FileOutputStream(archiveFile, false); |
| | | backupProperties.put(BACKUP_PROPERTY_ARCHIVE_FILENAME, archiveFilename); |
| | | } |
| | | catch (Exception e) |
| | | { |
| | | logger.traceException(e); |
| | | |
| | | LocalizableMessage message = ERR_JEB_BACKUP_CANNOT_CREATE_ARCHIVE_FILE. |
| | | get(archiveFilename, backupDir.getPath(), stackTraceToSingleLineString(e)); |
| | | throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), |
| | | message, e); |
| | | } |
| | | |
| | | // If we should encrypt the data, then wrap the output stream in a cipher |
| | | // output stream. |
| | | if (encrypt) |
| | | { |
| | | try |
| | | { |
| | | outputStream |
| | | = cryptoManager.getCipherOutputStream(outputStream); |
| | | } |
| | | catch (CryptoManagerException e) |
| | | { |
| | | logger.traceException(e); |
| | | |
| | | LocalizableMessage message = ERR_JEB_BACKUP_CANNOT_GET_CIPHER.get( |
| | | stackTraceToSingleLineString(e)); |
| | | throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), |
| | | message, e); |
| | | } |
| | | } |
| | | |
| | | |
| | | // Wrap the file output stream in a zip output stream. |
| | | ZipOutputStream zipStream = new ZipOutputStream(outputStream); |
| | | |
| | | LocalizableMessage message = ERR_JEB_BACKUP_ZIP_COMMENT.get( |
| | | DynamicConstants.PRODUCT_NAME, |
| | | backupID, backendID); |
| | | zipStream.setComment(message.toString()); |
| | | |
| | | if (compress) |
| | | { |
| | | zipStream.setLevel(Deflater.DEFAULT_COMPRESSION); |
| | | } |
| | | else |
| | | { |
| | | zipStream.setLevel(Deflater.NO_COMPRESSION); |
| | | } |
| | | |
| | | // Get a list of all the log files comprising the database. |
| | | FilenameFilter filenameFilter = new FilenameFilter() |
| | | { |
| | | public boolean accept(File d, String name) |
| | | { |
| | | return name.endsWith(".jdb"); |
| | | } |
| | | }; |
| | | |
| | | File[] logFiles; |
| | | try |
| | | { |
| | | logFiles = backendDir.listFiles(filenameFilter); |
| | | } |
| | | catch (Exception e) |
| | | { |
| | | logger.traceException(e); |
| | | |
| | | message = ERR_JEB_BACKUP_CANNOT_LIST_LOG_FILES.get( |
| | | backendDir.getAbsolutePath(), stackTraceToSingleLineString(e)); |
| | | throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), |
| | | message, e); |
| | | } |
| | | |
| | | // Check to see if backend is empty. If so, insert placeholder entry into |
| | | // archive |
| | | if(logFiles.length <= 0) |
| | | { |
| | | try |
| | | { |
| | | ZipEntry emptyPlaceholder = new ZipEntry(ZIPENTRY_EMPTY_PLACEHOLDER); |
| | | zipStream.putNextEntry(emptyPlaceholder); |
| | | } |
| | | catch (IOException e) |
| | | { |
| | | logger.traceException(e); |
| | | message = ERR_JEB_BACKUP_CANNOT_WRITE_ARCHIVE_FILE.get( |
| | | ZIPENTRY_EMPTY_PLACEHOLDER, stackTraceToSingleLineString(e)); |
| | | throw new DirectoryException( |
| | | DirectoryServer.getServerErrorResultCode(), message, e); |
| | | } |
| | | } |
| | | |
| | | // Sort the log files from oldest to youngest since this is the order |
| | | // in which they must be copied. |
| | | // This is easy since the files are created in alphabetical order by JE. |
| | | Arrays.sort(logFiles); |
| | | |
| | | try |
| | | { |
| | | // Process log files that are unchanged from the base backup. |
| | | int indexCurrent = 0; |
| | | if (latestFileName != null) |
| | | { |
| | | ArrayList<String> unchangedList = new ArrayList<String>(); |
| | | while (indexCurrent < logFiles.length && |
| | | !backupConfig.isCancelled()) |
| | | { |
| | | File logFile = logFiles[indexCurrent]; |
| | | String logFileName = logFile.getName(); |
| | | |
| | | // Stop when we get to the first log file that has been |
| | | // written since the base backup. |
| | | int compareResult = logFileName.compareTo(latestFileName); |
| | | if (compareResult > 0 || |
| | | (compareResult == 0 && logFile.length() != latestFileSize)) |
| | | { |
| | | break; |
| | | } |
| | | |
| | | logger.info(NOTE_JEB_BACKUP_FILE_UNCHANGED, logFileName); |
| | | |
| | | unchangedList.add(logFileName); |
| | | |
| | | indexCurrent++; |
| | | } |
| | | |
| | | // Write a file containing the list of unchanged log files. |
| | | if (!unchangedList.isEmpty()) |
| | | { |
| | | String zipEntryName = ZIPENTRY_UNCHANGED_LOGFILES; |
| | | try |
| | | { |
| | | archiveList(zipStream, mac, digest, zipEntryName, unchangedList); |
| | | } |
| | | catch (IOException e) |
| | | { |
| | | logger.traceException(e); |
| | | message = ERR_JEB_BACKUP_CANNOT_WRITE_ARCHIVE_FILE.get( |
| | | zipEntryName, stackTraceToSingleLineString(e)); |
| | | throw new DirectoryException( |
| | | DirectoryServer.getServerErrorResultCode(), message, e); |
| | | } |
| | | |
| | | // Set the dependency. |
| | | dependencies.add(baseBackup.getBackupID()); |
| | | } |
| | | } |
| | | |
| | | // Write the new log files to the zip file. |
| | | do |
| | | { |
| | | boolean deletedFiles = false; |
| | | |
| | | while (indexCurrent < logFiles.length && |
| | | !backupConfig.isCancelled()) |
| | | { |
| | | File logFile = logFiles[indexCurrent]; |
| | | |
| | | try |
| | | { |
| | | latestFileSize = archiveFile(zipStream, mac, digest, |
| | | logFile, backupConfig); |
| | | latestFileName = logFile.getName(); |
| | | } |
| | | catch (FileNotFoundException e) |
| | | { |
| | | logger.traceException(e); |
| | | |
| | | // A log file has been deleted by the cleaner since we started. |
| | | deletedFiles = true; |
| | | } |
| | | catch (IOException e) |
| | | { |
| | | logger.traceException(e); |
| | | message = ERR_JEB_BACKUP_CANNOT_WRITE_ARCHIVE_FILE.get( |
| | | logFile.getName(), stackTraceToSingleLineString(e)); |
| | | throw new DirectoryException( |
| | | DirectoryServer.getServerErrorResultCode(), message, e); |
| | | } |
| | | |
| | | indexCurrent++; |
| | | } |
| | | |
| | | if (deletedFiles) |
| | | { |
| | | /* |
| | | The cleaner is active and has deleted one or more of the log files |
| | | since we started. The in-use data from those log files will have |
| | | been written to new log files, so we must include those new files. |
| | | */ |
| | | final String latest = logFiles[logFiles.length-1].getName(); |
| | | FilenameFilter filter = new JELatestFileFilter(latest, |
| | | latestFileSize); |
| | | |
| | | try |
| | | { |
| | | logFiles = backendDir.listFiles(filter); |
| | | indexCurrent = 0; |
| | | } |
| | | catch (Exception e) |
| | | { |
| | | logger.traceException(e); |
| | | |
| | | message = ERR_JEB_BACKUP_CANNOT_LIST_LOG_FILES.get( |
| | | backendDir.getAbsolutePath(), stackTraceToSingleLineString(e)); |
| | | throw new DirectoryException( |
| | | DirectoryServer.getServerErrorResultCode(), message, e); |
| | | } |
| | | |
| | | if (logFiles == null) |
| | | { |
| | | break; |
| | | } |
| | | |
| | | Arrays.sort(logFiles); |
| | | |
| | | logger.info(NOTE_JEB_BACKUP_CLEANER_ACTIVITY, logFiles.length); |
| | | } |
| | | else |
| | | { |
| | | // We are done. |
| | | break; |
| | | } |
| | | } |
| | | while (true); |
| | | |
| | | } |
| | | // FIXME: The handling of exception below, plus the lack of finally block |
| | | // to close the zipStream is clumsy. Needs cleanup and best practice. |
| | | catch (DirectoryException e) |
| | | { |
| | | logger.traceException(e); |
| | | |
| | | try |
| | | { |
| | | zipStream.close(); |
| | | } catch (Exception e2) {} |
| | | } |
| | | |
| | | // We're done writing the file, so close the zip stream (which should also |
| | | // close the underlying stream). |
| | | try |
| | | { |
| | | zipStream.close(); |
| | | } |
| | | catch (Exception e) |
| | | { |
| | | logger.traceException(e); |
| | | |
| | | message = ERR_JEB_BACKUP_CANNOT_CLOSE_ZIP_STREAM. |
| | | get(archiveFilename, backupDir.getPath(), |
| | | stackTraceToSingleLineString(e)); |
| | | throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), |
| | | message, e); |
| | | } |
| | | |
| | | |
| | | // Get the digest or MAC bytes if appropriate. |
| | | byte[] digestBytes = null; |
| | | byte[] macBytes = null; |
| | | if (hash) |
| | | { |
| | | if (signHash) |
| | | { |
| | | macBytes = mac.doFinal(); |
| | | } |
| | | else |
| | | { |
| | | digestBytes = digest.digest(); |
| | | } |
| | | } |
| | | |
| | | |
| | | // Create a descriptor for this backup. |
| | | backupProperties.put(PROPERTY_LAST_LOGFILE_NAME, latestFileName); |
| | | backupProperties.put(PROPERTY_LAST_LOGFILE_SIZE, |
| | | String.valueOf(latestFileSize)); |
| | | BackupInfo backupInfo = new BackupInfo(backupDir, backupID, |
| | | backupDate, incremental, compress, |
| | | encrypt, digestBytes, macBytes, |
| | | dependencies, backupProperties); |
| | | |
| | | try |
| | | { |
| | | backupDir.addBackup(backupInfo); |
| | | backupDir.writeBackupDirectoryDescriptor(); |
| | | } |
| | | catch (Exception e) |
| | | { |
| | | logger.traceException(e); |
| | | |
| | | message = ERR_JEB_BACKUP_CANNOT_UPDATE_BACKUP_DESCRIPTOR.get( |
| | | backupDir.getDescriptorPath(), stackTraceToSingleLineString(e)); |
| | | throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), |
| | | message, e); |
| | | } |
| | | |
| | | // Remove the backup if this operation was cancelled since the |
| | | // backup may be incomplete |
| | | if (backupConfig.isCancelled()) |
| | | { |
| | | removeBackup(backupDir, backupID); |
| | | } |
| | | } |
| | | |
| | | |
| | | |
| | | /** |
| | | * Restore a JE backend from backup, or verify the backup. |
| | | * @param backendDir The configuration of the backend instance to be |
| | | * restored. |
| | | * @param restoreConfig The configuration to use when performing the restore. |
| | | * @throws DirectoryException If a Directory Server error occurs. |
| | | */ |
| | | public void restoreBackup(File backendDir, |
| | | RestoreConfig restoreConfig) |
| | | throws DirectoryException |
| | | { |
| | | // Get the properties to use for the restore. |
| | | String backupID = restoreConfig.getBackupID(); |
| | | BackupDirectory backupDir = restoreConfig.getBackupDirectory(); |
| | | boolean verifyOnly = restoreConfig.verifyOnly(); |
| | | |
| | | BackupInfo backupInfo = getBackupInfo(backupDir, backupID); |
| | | |
| | | // Create a restore directory with a different name to the backend |
| | | // directory. |
| | | File restoreDir = new File(backendDir.getPath() + "-restore-" + backupID); |
| | | if (!verifyOnly) |
| | | { |
| | | // FIXME: It's odd that we try to clean the directory before creating it |
| | | cleanup(restoreDir); |
| | | restoreDir.mkdir(); |
| | | } |
| | | |
| | | // Get the set of restore files that are in dependencies. |
| | | Set<String> includeFiles; |
| | | try |
| | | { |
| | | includeFiles = getUnchanged(backupDir, backupInfo); |
| | | } |
| | | catch (IOException e) |
| | | { |
| | | logger.traceException(e); |
| | | LocalizableMessage message = ERR_JEB_BACKUP_CANNOT_RESTORE.get( |
| | | backupInfo.getBackupID(), stackTraceToSingleLineString(e)); |
| | | throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), |
| | | message, e); |
| | | } |
| | | |
| | | // Restore any dependencies. |
| | | List<BackupInfo> dependents = getDependents(backupDir, backupInfo); |
| | | for (BackupInfo dependent : dependents) |
| | | { |
| | | try |
| | | { |
| | | restoreArchive(restoreDir, restoreConfig, dependent, includeFiles); |
| | | } |
| | | catch (IOException e) |
| | | { |
| | | logger.traceException(e); |
| | | LocalizableMessage message = ERR_JEB_BACKUP_CANNOT_RESTORE.get( |
| | | dependent.getBackupID(), stackTraceToSingleLineString(e)); |
| | | throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), |
| | | message, e); |
| | | } |
| | | } |
| | | |
| | | // Restore the final archive file. |
| | | try |
| | | { |
| | | restoreArchive(restoreDir, restoreConfig, backupInfo, null); |
| | | } |
| | | catch (IOException e) |
| | | { |
| | | logger.traceException(e); |
| | | LocalizableMessage message = ERR_JEB_BACKUP_CANNOT_RESTORE.get( |
| | | backupInfo.getBackupID(), stackTraceToSingleLineString(e)); |
| | | throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), |
| | | message, e); |
| | | } |
| | | |
| | | // Delete the current backend directory and rename the restore directory. |
| | | if (!verifyOnly) |
| | | { |
| | | StaticUtils.recursiveDelete(backendDir); |
| | | if (!restoreDir.renameTo(backendDir)) |
| | | { |
| | | LocalizableMessage msg = ERR_JEB_CANNOT_RENAME_RESTORE_DIRECTORY.get( |
| | | restoreDir.getPath(), backendDir.getPath()); |
| | | throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), |
| | | msg); |
| | | } |
| | | } |
| | | } |
| | | |
| | | private void cleanup(File directory) { |
| | | File[] files = directory.listFiles(); |
| | | if (files != null) |
| | | { |
| | | for (File f : files) |
| | | { |
| | | f.delete(); |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Removes the specified backup if it is possible to do so. |
| | | * |
| | | * @param backupDir The backup directory structure with which the |
| | | * specified backup is associated. |
| | | * @param backupID The backup ID for the backup to be removed. |
| | | * |
| | | * @throws DirectoryException If it is not possible to remove the specified |
| | | * backup for some reason (e.g., no such backup |
| | | * exists or there are other backups that are |
| | | * dependent upon it). |
| | | */ |
| | | public void removeBackup(BackupDirectory backupDir, |
| | | String backupID) |
| | | throws DirectoryException |
| | | { |
| | | try |
| | | { |
| | | backupDir.removeBackup(backupID); |
| | | } |
| | | catch (ConfigException e) |
| | | { |
| | | logger.traceException(e); |
| | | throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), |
| | | e.getMessageObject()); |
| | | } |
| | | |
| | | try |
| | | { |
| | | backupDir.writeBackupDirectoryDescriptor(); |
| | | } |
| | | catch (Exception e) |
| | | { |
| | | logger.traceException(e); |
| | | |
| | | LocalizableMessage message = ERR_JEB_BACKUP_CANNOT_UPDATE_BACKUP_DESCRIPTOR.get( |
| | | backupDir.getDescriptorPath(), stackTraceToSingleLineString(e)); |
| | | throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), |
| | | message, e); |
| | | } |
| | | |
| | | // Remove the archive file. |
| | | BackupInfo backupInfo = getBackupInfo(backupDir, backupID); |
| | | File archiveFile = getArchiveFile(backupDir, backupInfo); |
| | | archiveFile.delete(); |
| | | |
| | | } |
| | | |
| | | private File getArchiveFile(BackupDirectory backupDir, |
| | | BackupInfo backupInfo) { |
| | | Map<String,String> backupProperties = backupInfo.getBackupProperties(); |
| | | |
| | | String archiveFilename = |
| | | backupProperties.get(BACKUP_PROPERTY_ARCHIVE_FILENAME); |
| | | return new File(backupDir.getPath(), archiveFilename); |
| | | } |
| | | |
| | | |
| | | /** |
| | | * Restore the contents of an archive file. If the archive is being |
| | | * restored as a dependency, then only files in the specified set |
| | | * are restored, and the restored files are removed from the set. Otherwise |
| | | * all files from the archive are restored, and files that are to be found |
| | | * in dependencies are added to the set. |
| | | * |
| | | * @param restoreDir The directory in which files are to be restored. |
| | | * @param restoreConfig The restore configuration. |
| | | * @param backupInfo The backup containing the files to be restored. |
| | | * @param includeFiles The set of files to be restored. If null, then |
| | | * all files are restored. |
| | | * @throws DirectoryException If a Directory Server error occurs. |
| | | * @throws IOException If an I/O exception occurs during the restore. |
| | | */ |
| | | private void restoreArchive(File restoreDir, |
| | | RestoreConfig restoreConfig, |
| | | BackupInfo backupInfo, |
| | | Set<String> includeFiles) |
| | | throws DirectoryException,IOException |
| | | { |
| | | BackupDirectory backupDir = restoreConfig.getBackupDirectory(); |
| | | boolean verifyOnly = restoreConfig.verifyOnly(); |
| | | |
| | | String backupID = backupInfo.getBackupID(); |
| | | boolean encrypt = backupInfo.isEncrypted(); |
| | | byte[] hash = backupInfo.getUnsignedHash(); |
| | | byte[] signHash = backupInfo.getSignedHash(); |
| | | |
| | | HashMap<String,String> backupProperties = backupInfo.getBackupProperties(); |
| | | |
| | | String archiveFilename = |
| | | backupProperties.get(BACKUP_PROPERTY_ARCHIVE_FILENAME); |
| | | File archiveFile = new File(backupDir.getPath(), archiveFilename); |
| | | |
| | | InputStream inputStream = new FileInputStream(archiveFile); |
| | | |
| | | // Get the crypto manager and use it to obtain references to the message |
| | | // digest and/or MAC to use for hashing and/or signing. |
| | | CryptoManager cryptoManager = DirectoryServer.getCryptoManager(); |
| | | Mac mac = null; |
| | | MessageDigest digest = null; |
| | | |
| | | if (signHash != null) |
| | | { |
| | | String macKeyID = backupProperties.get(BACKUP_PROPERTY_MAC_KEY_ID); |
| | | |
| | | try |
| | | { |
| | | mac = cryptoManager.getMacEngine(macKeyID); |
| | | } |
| | | catch (Exception e) |
| | | { |
| | | logger.traceException(e); |
| | | |
| | | LocalizableMessage message = ERR_JEB_BACKUP_CANNOT_GET_MAC.get( |
| | | macKeyID, stackTraceToSingleLineString(e)); |
| | | throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), |
| | | message, e); |
| | | } |
| | | } |
| | | |
| | | if (hash != null) |
| | | { |
| | | String digestAlgorithm = backupProperties.get( |
| | | BACKUP_PROPERTY_DIGEST_ALGORITHM); |
| | | |
| | | try |
| | | { |
| | | digest = cryptoManager.getMessageDigest(digestAlgorithm); |
| | | } |
| | | catch (Exception e) |
| | | { |
| | | logger.traceException(e); |
| | | |
| | | LocalizableMessage message = ERR_JEB_BACKUP_CANNOT_GET_DIGEST.get( |
| | | digestAlgorithm, stackTraceToSingleLineString(e)); |
| | | throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), |
| | | message, e); |
| | | } |
| | | } |
| | | |
| | | |
| | | // If the data is encrypted, then wrap the input stream in a cipher |
| | | // input stream. |
| | | if (encrypt) |
| | | { |
| | | try |
| | | { |
| | | inputStream = cryptoManager.getCipherInputStream(inputStream); |
| | | } |
| | | catch (CryptoManagerException e) |
| | | { |
| | | logger.traceException(e); |
| | | |
| | | LocalizableMessage message = ERR_JEB_BACKUP_CANNOT_GET_CIPHER.get( |
| | | stackTraceToSingleLineString(e)); |
| | | throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), |
| | | message, e); |
| | | } |
| | | } |
| | | |
| | | |
| | | // Wrap the file input stream in a zip input stream. |
| | | ZipInputStream zipStream = new ZipInputStream(inputStream); |
| | | |
| | | // Iterate through the entries in the zip file. |
| | | ZipEntry zipEntry = zipStream.getNextEntry(); |
| | | while (zipEntry != null && !restoreConfig.isCancelled()) |
| | | { |
| | | String name = zipEntry.getName(); |
| | | |
| | | if (name.equals(ZIPENTRY_EMPTY_PLACEHOLDER)) |
| | | { |
| | | // This entry is treated specially to indicate a backup of an empty |
| | | // backend was attempted. |
| | | |
| | | zipEntry = zipStream.getNextEntry(); |
| | | continue; |
| | | } |
| | | |
| | | if (name.equals(ZIPENTRY_UNCHANGED_LOGFILES)) |
| | | { |
| | | // This entry is treated specially. It is never restored, |
| | | // and its hash is computed on the strings, not the bytes. |
| | | if (mac != null || digest != null) |
| | | { |
| | | // The file name is part of the hash. |
| | | if (mac != null) |
| | | { |
| | | mac.update(getBytes(name)); |
| | | } |
| | | |
| | | if (digest != null) |
| | | { |
| | | digest.update(getBytes(name)); |
| | | } |
| | | |
| | | InputStreamReader reader = new InputStreamReader(zipStream); |
| | | BufferedReader bufferedReader = new BufferedReader(reader); |
| | | String line = bufferedReader.readLine(); |
| | | while (line != null) |
| | | { |
| | | if (mac != null) |
| | | { |
| | | mac.update(getBytes(line)); |
| | | } |
| | | |
| | | if (digest != null) |
| | | { |
| | | digest.update(getBytes(line)); |
| | | } |
| | | |
| | | line = bufferedReader.readLine(); |
| | | } |
| | | } |
| | | |
| | | zipEntry = zipStream.getNextEntry(); |
| | | continue; |
| | | } |
| | | |
| | | // See if we need to restore the file. |
| | | File file = new File(restoreDir, name); |
| | | OutputStream outputStream = null; |
| | | if (includeFiles == null || includeFiles.contains(zipEntry.getName())) |
| | | { |
| | | if (!verifyOnly) |
| | | { |
| | | outputStream = new FileOutputStream(file); |
| | | } |
| | | } |
| | | |
| | | if (outputStream != null || mac != null || digest != null) |
| | | { |
| | | if (verifyOnly) |
| | | { |
| | | logger.info(NOTE_JEB_BACKUP_VERIFY_FILE, zipEntry.getName()); |
| | | } |
| | | |
| | | // The file name is part of the hash. |
| | | if (mac != null) |
| | | { |
| | | mac.update(getBytes(name)); |
| | | } |
| | | |
| | | if (digest != null) |
| | | { |
| | | digest.update(getBytes(name)); |
| | | } |
| | | |
| | | // Process the file. |
| | | long totalBytesRead = 0; |
| | | byte[] buffer = new byte[8192]; |
| | | int bytesRead = zipStream.read(buffer); |
| | | while (bytesRead > 0 && !restoreConfig.isCancelled()) |
| | | { |
| | | totalBytesRead += bytesRead; |
| | | |
| | | if (mac != null) |
| | | { |
| | | mac.update(buffer, 0, bytesRead); |
| | | } |
| | | |
| | | if (digest != null) |
| | | { |
| | | digest.update(buffer, 0, bytesRead); |
| | | } |
| | | |
| | | if (outputStream != null) |
| | | { |
| | | outputStream.write(buffer, 0, bytesRead); |
| | | } |
| | | |
| | | bytesRead = zipStream.read(buffer); |
| | | } |
| | | |
| | | if (outputStream != null) |
| | | { |
| | | outputStream.close(); |
| | | |
| | | logger.info(NOTE_JEB_BACKUP_RESTORED_FILE, zipEntry.getName(), totalBytesRead); |
| | | } |
| | | } |
| | | |
| | | zipEntry = zipStream.getNextEntry(); |
| | | } |
| | | |
| | | zipStream.close(); |
| | | |
| | | // Check the hash. |
| | | if (digest != null) |
| | | { |
| | | if (!Arrays.equals(digest.digest(), hash)) |
| | | { |
| | | LocalizableMessage message = ERR_JEB_BACKUP_UNSIGNED_HASH_ERROR.get(backupID); |
| | | throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), |
| | | message); |
| | | } |
| | | } |
| | | |
| | | if (mac != null) |
| | | { |
| | | byte[] computedSignHash = mac.doFinal(); |
| | | |
| | | if (!Arrays.equals(computedSignHash, signHash)) |
| | | { |
| | | LocalizableMessage message = ERR_JEB_BACKUP_SIGNED_HASH_ERROR.get(backupID); |
| | | throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), |
| | | message); |
| | | } |
| | | } |
| | | } |
| | | |
| | | |
| | | |
| | | /** |
| | | * Writes a file to an entry in the archive file. |
| | | * @param zipStream The zip output stream to which the file is to be |
| | | * written. |
| | | * @param mac A message authentication code to be updated, if not null. |
| | | * @param digest A message digest to be updated, if not null. |
| | | * @param file The file to be written. |
| | | * @return The number of bytes written from the file. |
| | | * @throws FileNotFoundException If the file to be archived does not exist. |
| | | * @throws IOException If an I/O error occurs while archiving the file. |
| | | */ |
| | | private long archiveFile(ZipOutputStream zipStream, |
| | | Mac mac, MessageDigest digest, File file, |
| | | BackupConfig backupConfig) |
| | | throws IOException, FileNotFoundException |
| | | { |
| | | ZipEntry zipEntry = new ZipEntry(file.getName()); |
| | | |
| | | // Open the file for reading. |
| | | InputStream inputStream = new FileInputStream(file); |
| | | |
| | | // Start the zip entry. |
| | | zipStream.putNextEntry(zipEntry); |
| | | |
| | | // Put the name in the hash. |
| | | if (mac != null) |
| | | { |
| | | mac.update(getBytes(file.getName())); |
| | | } |
| | | |
| | | if (digest != null) |
| | | { |
| | | digest.update(getBytes(file.getName())); |
| | | } |
| | | |
| | | // Write the file. |
| | | long totalBytesRead = 0; |
| | | byte[] buffer = new byte[8192]; |
| | | int bytesRead = inputStream.read(buffer); |
| | | while (bytesRead > 0 && !backupConfig.isCancelled()) |
| | | { |
| | | if (mac != null) |
| | | { |
| | | mac.update(buffer, 0, bytesRead); |
| | | } |
| | | |
| | | if (digest != null) |
| | | { |
| | | digest.update(buffer, 0, bytesRead); |
| | | } |
| | | |
| | | zipStream.write(buffer, 0, bytesRead); |
| | | totalBytesRead += bytesRead; |
| | | bytesRead = inputStream.read(buffer); |
| | | } |
| | | inputStream.close(); |
| | | |
| | | // Finish the zip entry. |
| | | zipStream.closeEntry(); |
| | | |
| | | logger.info(NOTE_JEB_BACKUP_ARCHIVED_FILE, zipEntry.getName()); |
| | | |
| | | return totalBytesRead; |
| | | } |
| | | |
| | | /** |
| | | * Write a list of strings to an entry in the archive file. |
| | | * @param zipStream The zip output stream to which the entry is to be |
| | | * written. |
| | | * @param mac An optional MAC to be updated. |
| | | * @param digest An optional message digest to be updated. |
| | | * @param fileName The name of the zip entry to be written. |
| | | * @param list A list of strings to be written. The strings must not |
| | | * contain newlines. |
| | | * @throws IOException If an I/O error occurs while writing the archive entry. |
| | | */ |
| | | private void archiveList(ZipOutputStream zipStream, |
| | | Mac mac, MessageDigest digest, String fileName, |
| | | List<String> list) |
| | | throws IOException |
| | | { |
| | | ZipEntry zipEntry = new ZipEntry(fileName); |
| | | |
| | | // Start the zip entry. |
| | | zipStream.putNextEntry(zipEntry); |
| | | |
| | | // Put the name in the hash. |
| | | if (mac != null) |
| | | { |
| | | mac.update(getBytes(fileName)); |
| | | } |
| | | |
| | | if (digest != null) |
| | | { |
| | | digest.update(getBytes(fileName)); |
| | | } |
| | | |
| | | Writer writer = new OutputStreamWriter(zipStream); |
| | | for (String s : list) |
| | | { |
| | | if (mac != null) |
| | | { |
| | | mac.update(getBytes(s)); |
| | | } |
| | | |
| | | if (digest != null) |
| | | { |
| | | digest.update(getBytes(s)); |
| | | } |
| | | |
| | | writer.write(s); |
| | | writer.write(EOL); |
| | | } |
| | | writer.flush(); |
| | | |
| | | // Finish the zip entry. |
| | | zipStream.closeEntry(); |
| | | } |
| | | |
| | | /** |
| | | * Obtains the set of files in a backup that are unchanged from its |
| | | * dependent backup or backups. This list is stored as the first entry |
| | | * in the archive file. |
| | | * @param backupDir The backup directory. |
| | | * @param backupInfo The backup info. |
| | | * @return The set of files that were unchanged. |
| | | * @throws DirectoryException If an error occurs while trying to get the |
| | | * appropriate cipher algorithm for an encrypted backup. |
| | | * @throws IOException If an I/O error occurs while reading the backup |
| | | * archive file. |
| | | */ |
| | | private Set<String> getUnchanged(BackupDirectory backupDir, |
| | | BackupInfo backupInfo) |
| | | throws DirectoryException, IOException |
| | | { |
| | | HashSet<String> hashSet = new HashSet<String>(); |
| | | |
| | | boolean encrypt = backupInfo.isEncrypted(); |
| | | |
| | | File archiveFile = getArchiveFile(backupDir, backupInfo); |
| | | |
| | | InputStream inputStream = new FileInputStream(archiveFile); |
| | | |
| | | // Get the crypto manager and use it to obtain references to the message |
| | | // digest and/or MAC to use for hashing and/or signing. |
| | | CryptoManager cryptoManager = DirectoryServer.getCryptoManager(); |
| | | |
| | | // If the data is encrypted, then wrap the input stream in a cipher |
| | | // input stream. |
| | | if (encrypt) |
| | | { |
| | | try |
| | | { |
| | | inputStream = cryptoManager.getCipherInputStream(inputStream); |
| | | } |
| | | catch (CryptoManagerException e) |
| | | { |
| | | logger.traceException(e); |
| | | |
| | | LocalizableMessage message = ERR_JEB_BACKUP_CANNOT_GET_CIPHER.get( |
| | | stackTraceToSingleLineString(e)); |
| | | throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), |
| | | message, e); |
| | | } |
| | | } |
| | | |
| | | |
| | | // Wrap the file input stream in a zip input stream. |
| | | ZipInputStream zipStream = new ZipInputStream(inputStream); |
| | | |
| | | // Iterate through the entries in the zip file. |
| | | ZipEntry zipEntry = zipStream.getNextEntry(); |
| | | while (zipEntry != null) |
| | | { |
| | | // We are looking for the entry containing the list of unchanged files. |
| | | if (zipEntry.getName().equals(ZIPENTRY_UNCHANGED_LOGFILES)) |
| | | { |
| | | InputStreamReader reader = new InputStreamReader(zipStream); |
| | | BufferedReader bufferedReader = new BufferedReader(reader); |
| | | String line = bufferedReader.readLine(); |
| | | while (line != null) |
| | | { |
| | | hashSet.add(line); |
| | | line = bufferedReader.readLine(); |
| | | } |
| | | break; |
| | | } |
| | | |
| | | zipEntry = zipStream.getNextEntry(); |
| | | } |
| | | |
| | | zipStream.close(); |
| | | return hashSet; |
| | | } |
| | | |
| | | /** |
| | | * Obtains a list of the dependencies of a given backup in order from |
| | | * the oldest (the full backup), to the most recent. |
| | | * @param backupDir The backup directory. |
| | | * @param backupInfo The backup for which dependencies are required. |
| | | * @return A list of dependent backups. |
| | | * @throws DirectoryException If a Directory Server error occurs. |
| | | */ |
| | | private ArrayList<BackupInfo> getDependents(BackupDirectory backupDir, |
| | | BackupInfo backupInfo) |
| | | throws DirectoryException |
| | | { |
| | | ArrayList<BackupInfo> dependents = new ArrayList<BackupInfo>(); |
| | | while (backupInfo != null && !backupInfo.getDependencies().isEmpty()) |
| | | { |
| | | String backupID = backupInfo.getDependencies().iterator().next(); |
| | | backupInfo = getBackupInfo(backupDir, backupID); |
| | | if (backupInfo != null) |
| | | { |
| | | dependents.add(backupInfo); |
| | | } |
| | | } |
| | | Collections.reverse(dependents); |
| | | return dependents; |
| | | } |
| | | |
| | | /** |
| | | * Get the information for a given backup ID from the backup directory. |
| | | * @param backupDir The backup directory. |
| | | * @param backupID The backup ID. |
| | | * @return The backup information, never null. |
| | | * @throws DirectoryException If the backup information cannot be found. |
| | | */ |
| | | private BackupInfo getBackupInfo(BackupDirectory backupDir, |
| | | String backupID) throws DirectoryException |
| | | { |
| | | BackupInfo backupInfo = backupDir.getBackupInfo(backupID); |
| | | if (backupInfo == null) |
| | | { |
| | | LocalizableMessage message = |
| | | ERR_JEB_BACKUP_MISSING_BACKUPID.get(backupDir.getPath(), backupID); |
| | | throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), |
| | | message); |
| | | } |
| | | return backupInfo; |
| | | } |
| | | |
| | | /** |
| | | * This class implements a FilenameFilter to detect the last file |
| | | * from a JE database. |
| | | */ |
| | | private static class JELatestFileFilter implements FilenameFilter { |
| | | private final String latest; |
| | | private final long latestSize; |
| | | |
| | | public JELatestFileFilter(String latest, long latestSize) { |
| | | this.latest = latest; |
| | | this.latestSize = latestSize; |
| | | } |
| | | |
| | | public boolean accept(File d, String name) |
| | | { |
| | | if (!name.endsWith(".jdb")) return false; |
| | | int compareTo = name.compareTo(latest); |
| | | return compareTo > 0 || compareTo == 0 && d.length() > latestSize; |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | /* |
| | | * CDDL HEADER START |
| | | * |
| | | * The contents of this file are subject to the terms of the |
| | | * Common Development and Distribution License, Version 1.0 only |
| | | * (the "License"). You may not use this file except in compliance |
| | | * with the License. |
| | | * |
| | | * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt |
| | | * or http://forgerock.org/license/CDDLv1.0.html. |
| | | * See the License for the specific language governing permissions |
| | | * and limitations under the License. |
| | | * |
| | | * When distributing Covered Code, include this CDDL HEADER in each |
| | | * file and include the License file at legal-notices/CDDLv1_0.txt. |
| | | * If applicable, add the following below this CDDL HEADER, with the |
| | | * fields enclosed by brackets "[]" replaced with your own identifying |
| | | * information: |
| | | * Portions Copyright [yyyy] [name of copyright owner] |
| | | * |
| | | * CDDL HEADER END |
| | | * |
| | | * |
| | | * Copyright 2006-2010 Sun Microsystems, Inc. |
| | | * Portions Copyright 2010-2014 ForgeRock AS. |
| | | */ |
| | | package org.opends.server.backends.pluggable; |
| | | |
| | | import java.lang.reflect.Method; |
| | | import java.math.BigInteger; |
| | | import java.util.*; |
| | | import java.util.concurrent.TimeUnit; |
| | | import java.util.logging.Level; |
| | | import java.util.logging.Logger; |
| | | |
| | | import org.forgerock.i18n.LocalizableMessage; |
| | | import org.forgerock.i18n.slf4j.LocalizedLogger; |
| | | import org.opends.server.admin.BooleanPropertyDefinition; |
| | | import org.opends.server.admin.DurationPropertyDefinition; |
| | | import org.opends.server.admin.PropertyDefinition; |
| | | import org.opends.server.admin.std.meta.LocalDBBackendCfgDefn; |
| | | import org.opends.server.admin.std.server.LocalDBBackendCfg; |
| | | import org.opends.server.config.ConfigConstants; |
| | | import org.forgerock.opendj.config.server.ConfigException; |
| | | |
| | | |
| | | |
| | | |
| | | import static com.sleepycat.je.EnvironmentConfig.*; |
| | | |
| | | import static org.opends.messages.BackendMessages.*; |
| | | import static org.opends.messages.ConfigMessages.*; |
| | | import static org.opends.messages.JebMessages.*; |
| | | |
| | | /** |
| | | * This class maps JE properties to configuration attributes. |
| | | */ |
| | | public class ConfigurableEnvironment |
| | | { |
| | | private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); |
| | | |
| | | /** |
| | | * The name of the attribute which configures the database cache size as a |
| | | * percentage of Java VM heap size. |
| | | */ |
| | | public static final String ATTR_DATABASE_CACHE_PERCENT = |
| | | ConfigConstants.NAME_PREFIX_CFG + "db-cache-percent"; |
| | | |
| | | /** |
| | | * The name of the attribute which configures the database cache size as an |
| | | * approximate number of bytes. |
| | | */ |
| | | public static final String ATTR_DATABASE_CACHE_SIZE = |
| | | ConfigConstants.NAME_PREFIX_CFG + "db-cache-size"; |
| | | |
| | | /** |
| | | * The name of the attribute which configures whether data updated by a |
| | | * database transaction is forced to disk. |
| | | */ |
| | | public static final String ATTR_DATABASE_TXN_NO_SYNC = |
| | | ConfigConstants.NAME_PREFIX_CFG + "db-txn-no-sync"; |
| | | |
| | | /** |
| | | * The name of the attribute which configures whether data updated by a |
| | | * database transaction is written from the Java VM to the O/S. |
| | | */ |
| | | public static final String ATTR_DATABASE_TXN_WRITE_NO_SYNC = |
| | | ConfigConstants.NAME_PREFIX_CFG + "db-txn-write-no-sync"; |
| | | |
| | | /** |
| | | * The name of the attribute which configures whether the database background |
| | | * cleaner thread runs. |
| | | */ |
| | | public static final String ATTR_DATABASE_RUN_CLEANER = |
| | | ConfigConstants.NAME_PREFIX_CFG + "db-run-cleaner"; |
| | | |
| | | /** |
| | | * The name of the attribute which configures the minimum percentage of log |
| | | * space that must be used in log files. |
| | | */ |
| | | public static final String ATTR_CLEANER_MIN_UTILIZATION = |
| | | ConfigConstants.NAME_PREFIX_CFG + "db-cleaner-min-utilization"; |
| | | |
| | | /** |
| | | * The name of the attribute which configures the maximum size of each |
| | | * individual JE log file, in bytes. |
| | | */ |
| | | public static final String ATTR_DATABASE_LOG_FILE_MAX = |
| | | ConfigConstants.NAME_PREFIX_CFG + "db-log-file-max"; |
| | | |
| | | /** |
| | | * The name of the attribute which configures the database cache eviction |
| | | * algorithm. |
| | | */ |
| | | public static final String ATTR_EVICTOR_LRU_ONLY = |
| | | ConfigConstants.NAME_PREFIX_CFG + "db-evictor-lru-only"; |
| | | |
| | | /** |
| | | * The name of the attribute which configures the number of nodes in one scan |
| | | * of the database cache evictor. |
| | | */ |
| | | public static final String ATTR_EVICTOR_NODES_PER_SCAN = |
| | | ConfigConstants.NAME_PREFIX_CFG + "db-evictor-nodes-per-scan"; |
| | | |
| | | /** |
| | | * The name of the attribute which configures the minimum number of threads |
| | | * of the database cache evictor pool. |
| | | */ |
| | | public static final String ATTR_EVICTOR_CORE_THREADS = |
| | | ConfigConstants.NAME_PREFIX_CFG + "db-evictor-core-threads"; |
| | | /** |
| | | * The name of the attribute which configures the maximum number of threads |
| | | * of the database cache evictor pool. |
| | | */ |
| | | public static final String ATTR_EVICTOR_MAX_THREADS = |
| | | ConfigConstants.NAME_PREFIX_CFG + "db-evictor-max-threads"; |
| | | |
| | | /** |
| | | * The name of the attribute which configures the time excess threads |
| | | * of the database cache evictor pool are kept alive. |
| | | */ |
| | | public static final String ATTR_EVICTOR_KEEP_ALIVE = |
| | | ConfigConstants.NAME_PREFIX_CFG + "db-evictor-keep-alive"; |
| | | |
| | | /** |
| | | * The name of the attribute which configures whether the logging file |
| | | * handler will be on or off. |
| | | */ |
| | | public static final String ATTR_LOGGING_FILE_HANDLER_ON = |
| | | ConfigConstants.NAME_PREFIX_CFG + "db-logging-file-handler-on"; |
| | | |
| | | |
| | | /** |
| | | * The name of the attribute which configures the trace logging message level. |
| | | */ |
| | | public static final String ATTR_LOGGING_LEVEL = |
| | | ConfigConstants.NAME_PREFIX_CFG + "db-logging-level"; |
| | | |
| | | |
| | | /** |
| | | * The name of the attribute which configures how many bytes are written to |
| | | * the log before the checkpointer runs. |
| | | */ |
| | | public static final String ATTR_CHECKPOINTER_BYTES_INTERVAL = |
| | | ConfigConstants.NAME_PREFIX_CFG + "db-checkpointer-bytes-interval"; |
| | | |
| | | |
| | | /** |
| | | * The name of the attribute which configures the amount of time between |
| | | * runs of the checkpointer. |
| | | */ |
| | | public static final String ATTR_CHECKPOINTER_WAKEUP_INTERVAL = |
| | | ConfigConstants.NAME_PREFIX_CFG + |
| | | "db-checkpointer-wakeup-interval"; |
| | | |
| | | |
| | | /** |
| | | * The name of the attribute which configures the number of lock tables. |
| | | */ |
| | | public static final String ATTR_NUM_LOCK_TABLES = |
| | | ConfigConstants.NAME_PREFIX_CFG + "db-num-lock-tables"; |
| | | |
| | | |
| | | /** |
| | | * The name of the attribute which configures the number threads |
| | | * allocated by the cleaner for log file processing. |
| | | */ |
| | | public static final String ATTR_NUM_CLEANER_THREADS = |
| | | ConfigConstants.NAME_PREFIX_CFG + "db-num-cleaner-threads"; |
| | | |
| | | /** |
| | | * The name of the attribute which configures the size of the file |
| | | * handle cache. |
| | | */ |
| | | public static final String ATTR_LOG_FILECACHE_SIZE = |
| | | ConfigConstants.NAME_PREFIX_CFG + "db-log-filecache-size"; |
| | | |
| | | |
| | | /** |
| | | * The name of the attribute which may specify any native JE properties. |
| | | */ |
| | | public static final String ATTR_JE_PROPERTY = |
| | | ConfigConstants.NAME_PREFIX_CFG + "je-property"; |
| | | |
| | | |
| | | /** |
| | | * A map of JE property names to the corresponding configuration attribute. |
| | | */ |
| | | private static HashMap<String, String> attrMap = |
| | | new HashMap<String, String>(); |
| | | |
| | | /** |
| | | * A map of configuration attribute names to the corresponding configuration |
| | | * object getter method. |
| | | */ |
| | | private static HashMap<String,Method> methodMap = |
| | | new HashMap<String, Method>(); |
| | | |
| | | /** |
| | | * A map of configuration attribute names to the corresponding configuration |
| | | * PropertyDefinition. |
| | | */ |
| | | private static HashMap<String,PropertyDefinition> defnMap = |
| | | new HashMap<String, PropertyDefinition>(); |
| | | |
| | | |
| | | // Pulled from resource/admin/ABBREVIATIONS.xsl. db is mose common. |
| | | private static final List<String> ABBREVIATIONS = Arrays.asList(new String[] |
| | | {"aci", "ip", "ssl", "dn", "rdn", "jmx", "smtp", "http", |
| | | "https", "ldap", "ldaps", "ldif", "jdbc", "tcp", "tls", |
| | | "pkcs11", "sasl", "gssapi", "md5", "je", "dse", "fifo", |
| | | "vlv", "uuid", "md5", "sha1", "sha256", "sha384", "sha512", |
| | | "tls", "db"}); |
| | | |
| | | /* |
| | | * e.g. db-cache-percent -> DBCachePercent |
| | | */ |
| | | private static String propNametoCamlCase(String hyphenated) |
| | | { |
| | | String[] components = hyphenated.split("\\-"); |
| | | StringBuilder buffer = new StringBuilder(); |
| | | for (String component: components) { |
| | | if (ABBREVIATIONS.contains(component)) { |
| | | buffer.append(component.toUpperCase()); |
| | | } else { |
| | | buffer.append(component.substring(0, 1).toUpperCase() + |
| | | component.substring(1)); |
| | | } |
| | | } |
| | | return buffer.toString(); |
| | | } |
| | | |
| | | |
| | | /** |
| | | * Register a JE property and its corresponding configuration attribute. |
| | | * |
| | | * @param propertyName The name of the JE property to be registered. |
| | | * @param attrName The name of the configuration attribute associated |
| | | * with the property. |
| | | * @throws Exception If there is an error in the attribute name. |
| | | */ |
| | | private static void registerProp(String propertyName, String attrName) |
| | | throws Exception |
| | | { |
| | | // Strip off NAME_PREFIX_CFG. |
| | | String baseName = attrName.substring(7); |
| | | |
| | | String methodBaseName = propNametoCamlCase(baseName); |
| | | |
| | | Class<LocalDBBackendCfg> configClass = LocalDBBackendCfg.class; |
| | | LocalDBBackendCfgDefn defn = LocalDBBackendCfgDefn.getInstance(); |
| | | Class<? extends LocalDBBackendCfgDefn> defClass = defn.getClass(); |
| | | |
| | | PropertyDefinition propDefn = |
| | | (PropertyDefinition)defClass.getMethod("get" + methodBaseName + |
| | | "PropertyDefinition").invoke(defn); |
| | | |
| | | String methodName; |
| | | if (propDefn instanceof BooleanPropertyDefinition) |
| | | { |
| | | methodName = "is" + methodBaseName; |
| | | } |
| | | else |
| | | { |
| | | methodName = "get" + methodBaseName; |
| | | } |
| | | |
| | | defnMap.put(attrName, propDefn); |
| | | methodMap.put(attrName, configClass.getMethod(methodName)); |
| | | attrMap.put(propertyName, attrName); |
| | | } |
| | | |
| | | |
| | | /** |
| | | * Get the name of the configuration attribute associated with a JE property. |
| | | * @param jeProperty The name of the JE property. |
| | | * @return The name of the associated configuration attribute. |
| | | */ |
| | | public static String getAttributeForProperty(String jeProperty) |
| | | { |
| | | return attrMap.get(jeProperty); |
| | | } |
| | | |
| | | /** |
| | | * Get the value of a JE property that is mapped to a configuration attribute. |
| | | * @param cfg The configuration containing the property values. |
| | | * @param attrName The conriguration attribute type name. |
| | | * @return The string value of the JE property. |
| | | */ |
| | | private static String getPropertyValue(LocalDBBackendCfg cfg, String attrName) |
| | | { |
| | | try |
| | | { |
| | | PropertyDefinition propDefn = defnMap.get(attrName); |
| | | Method method = methodMap.get(attrName); |
| | | |
| | | if (propDefn instanceof DurationPropertyDefinition) |
| | | { |
| | | Long value = (Long)method.invoke(cfg); |
| | | |
| | | // JE durations are in microseconds so we must convert. |
| | | DurationPropertyDefinition durationPropDefn = |
| | | (DurationPropertyDefinition)propDefn; |
| | | value = 1000*durationPropDefn.getBaseUnit().toMilliSeconds(value); |
| | | |
| | | return String.valueOf(value); |
| | | } |
| | | else |
| | | { |
| | | Object value = method.invoke(cfg); |
| | | |
| | | if (attrName.equals(ATTR_NUM_CLEANER_THREADS) && value == null) |
| | | { |
| | | // Automatically choose based on the number of processors. We will use |
| | | // similar heuristics to those used to define the default number of |
| | | // worker threads. |
| | | int cpus = Runtime.getRuntime().availableProcessors(); |
| | | value = Integer.valueOf(Math.max(24, cpus * 2)); |
| | | |
| | | logger.debug(INFO_ERGONOMIC_SIZING_OF_JE_CLEANER_THREADS, |
| | | cfg.dn().rdn().getAttributeValue(0), (Number) value); |
| | | } |
| | | else if (attrName.equals(ATTR_NUM_LOCK_TABLES) |
| | | && value == null) |
| | | { |
| | | // Automatically choose based on the number of processors. |
| | | // We'll assume that the user has also allowed automatic |
| | | // configuration of cleaners and workers. |
| | | int cpus = Runtime.getRuntime().availableProcessors(); |
| | | int cleaners = Math.max(24, cpus * 2); |
| | | int workers = Math.max(24, cpus * 2); |
| | | BigInteger tmp = BigInteger.valueOf((cleaners + workers) * 2); |
| | | value = tmp.nextProbablePrime(); |
| | | |
| | | logger.debug(INFO_ERGONOMIC_SIZING_OF_JE_LOCK_TABLES, cfg.dn().rdn().getAttributeValue(0), (Number) value); |
| | | } |
| | | |
| | | return String.valueOf(value); |
| | | } |
| | | } |
| | | catch (Exception e) |
| | | { |
| | | logger.traceException(e); |
| | | return ""; |
| | | } |
| | | } |
| | | |
| | | |
| | | |
| | | static |
| | | { |
| | | // Register the parameters that have JE property names. |
| | | try |
| | | { |
| | | registerProp("je.maxMemoryPercent", ATTR_DATABASE_CACHE_PERCENT); |
| | | registerProp("je.maxMemory", ATTR_DATABASE_CACHE_SIZE); |
| | | registerProp("je.cleaner.minUtilization", ATTR_CLEANER_MIN_UTILIZATION); |
| | | registerProp("je.env.runCleaner", ATTR_DATABASE_RUN_CLEANER); |
| | | registerProp("je.evictor.lruOnly", ATTR_EVICTOR_LRU_ONLY); |
| | | registerProp("je.evictor.nodesPerScan", ATTR_EVICTOR_NODES_PER_SCAN); |
| | | registerProp("je.evictor.coreThreads", ATTR_EVICTOR_CORE_THREADS); |
| | | registerProp("je.evictor.maxThreads", ATTR_EVICTOR_MAX_THREADS); |
| | | registerProp("je.evictor.keepAlive", ATTR_EVICTOR_KEEP_ALIVE); |
| | | registerProp("je.log.fileMax", ATTR_DATABASE_LOG_FILE_MAX); |
| | | registerProp("je.checkpointer.bytesInterval", |
| | | ATTR_CHECKPOINTER_BYTES_INTERVAL); |
| | | registerProp("je.checkpointer.wakeupInterval", |
| | | ATTR_CHECKPOINTER_WAKEUP_INTERVAL); |
| | | registerProp("je.lock.nLockTables", ATTR_NUM_LOCK_TABLES); |
| | | registerProp("je.cleaner.threads", ATTR_NUM_CLEANER_THREADS); |
| | | registerProp("je.log.fileCacheSize", ATTR_LOG_FILECACHE_SIZE); |
| | | } |
| | | catch (Exception e) |
| | | { |
| | | logger.traceException(e); |
| | | } |
| | | } |
| | | |
| | | |
| | | |
| | | /** |
| | | * Create a JE environment configuration with default values. |
| | | * |
| | | * @return A JE environment config containing default values. |
| | | */ |
| | | public static EnvironmentConfig defaultConfig() |
| | | { |
| | | EnvironmentConfig envConfig = new EnvironmentConfig(); |
| | | |
| | | envConfig.setTransactional(true); |
| | | envConfig.setAllowCreate(true); |
| | | |
| | | // "je.env.sharedLatches" is "true" by default since JE #12136 (3.3.62?) |
| | | |
| | | // This parameter was set to false while diagnosing a Berkeley DB JE bug. |
| | | // Normally cleansed log files are deleted, but if this is set false |
| | | // they are instead renamed from .jdb to .del. |
| | | envConfig.setConfigParam(CLEANER_EXPUNGE, "true"); |
| | | |
| | | // Under heavy write load the check point can fall behind causing |
| | | // uncontrolled DB growth over time. This parameter makes the out of |
| | | // the box configuration more robust at the cost of a slight |
| | | // reduction in maximum write throughput. Experiments have shown |
| | | // that response time predictability is not impacted negatively. |
| | | envConfig.setConfigParam(CHECKPOINTER_HIGH_PRIORITY, "true"); |
| | | |
| | | // If the JVM is reasonably large then we can safely default to |
| | | // bigger read buffers. This will result in more scalable checkpointer |
| | | // and cleaner performance. |
| | | if (Runtime.getRuntime().maxMemory() > 256 * 1024 * 1024) |
| | | { |
| | | envConfig.setConfigParam(CLEANER_LOOK_AHEAD_CACHE_SIZE, |
| | | String.valueOf(2 * 1024 * 1024)); |
| | | envConfig.setConfigParam(LOG_ITERATOR_READ_SIZE, |
| | | String.valueOf(2 * 1024 * 1024)); |
| | | envConfig.setConfigParam(LOG_FAULT_READ_SIZE, String.valueOf(4 * 1024)); |
| | | } |
| | | |
| | | // Disable lock timeouts, meaning that no lock wait |
| | | // timelimit is enforced and a deadlocked operation |
| | | // will block indefinitely. |
| | | envConfig.setLockTimeout(0, TimeUnit.MICROSECONDS); |
| | | |
| | | return envConfig; |
| | | } |
| | | |
| | | |
| | | |
| | | /** |
| | | * Parse a configuration associated with a JE environment and create an |
| | | * environment config from it. |
| | | * |
| | | * @param cfg The configuration to be parsed. |
| | | * @return An environment config instance corresponding to the config entry. |
| | | * @throws ConfigException If there is an error in the provided configuration |
| | | * entry. |
| | | */ |
| | | public static EnvironmentConfig parseConfigEntry(LocalDBBackendCfg cfg) |
| | | throws ConfigException |
| | | { |
| | | // See if the db cache size setting is valid. |
| | | if(cfg.getDBCacheSize() != 0) |
| | | { |
| | | if (MemoryBudget.getRuntimeMaxMemory() < cfg.getDBCacheSize()) { |
| | | throw new ConfigException( |
| | | ERR_CONFIG_JEB_CACHE_SIZE_GREATER_THAN_JVM_HEAP.get( |
| | | cfg.getDBCacheSize(), MemoryBudget.getRuntimeMaxMemory())); |
| | | } |
| | | if (cfg.getDBCacheSize() < MemoryBudget.MIN_MAX_MEMORY_SIZE) { |
| | | throw new ConfigException( |
| | | ERR_CONFIG_JEB_CACHE_SIZE_TOO_SMALL.get( |
| | | cfg.getDBCacheSize(), MemoryBudget.MIN_MAX_MEMORY_SIZE)); |
| | | } |
| | | } |
| | | |
| | | EnvironmentConfig envConfig = defaultConfig(); |
| | | |
| | | // Durability settings. |
| | | if (cfg.isDBTxnNoSync() && cfg.isDBTxnWriteNoSync()) |
| | | { |
| | | throw new ConfigException( |
| | | ERR_CONFIG_JEB_DURABILITY_CONFLICT.get()); |
| | | } |
| | | if (cfg.isDBTxnNoSync()) |
| | | { |
| | | envConfig.setDurability(Durability.COMMIT_NO_SYNC); |
| | | } |
| | | if (cfg.isDBTxnWriteNoSync()) |
| | | { |
| | | envConfig.setDurability(Durability.COMMIT_WRITE_NO_SYNC); |
| | | } |
| | | |
| | | // Iterate through the config attributes associated with a JE property. |
| | | for (Map.Entry<String, String> mapEntry : attrMap.entrySet()) |
| | | { |
| | | String jeProperty = mapEntry.getKey(); |
| | | String attrName = mapEntry.getValue(); |
| | | |
| | | String value = getPropertyValue(cfg, attrName); |
| | | envConfig.setConfigParam(jeProperty, value); |
| | | } |
| | | |
| | | // Set logging and file handler levels. |
| | | Logger parent = Logger.getLogger("com.sleepycat.je"); |
| | | try |
| | | { |
| | | parent.setLevel(Level.parse(cfg.getDBLoggingLevel())); |
| | | } |
| | | catch (Exception e) |
| | | { |
| | | throw new ConfigException(ERR_JEB_INVALID_LOGGING_LEVEL.get(cfg.getDBLoggingLevel(), cfg.dn())); |
| | | } |
| | | |
| | | final Level level = cfg.isDBLoggingFileHandlerOn() ? Level.ALL : Level.OFF; |
| | | envConfig.setConfigParam(FILE_LOGGING_LEVEL, level.getName()); |
| | | |
| | | // See if there are any native JE properties specified in the config |
| | | // and if so try to parse, evaluate and set them. |
| | | return setJEProperties(envConfig, cfg.getJEProperty(), attrMap); |
| | | } |
| | | |
| | | |
| | | |
| | | /** |
| | | * Parse, validate and set native JE environment properties for |
| | | * a given environment config. |
| | | * |
| | | * @param envConfig The JE environment config for which to set |
| | | * the properties. |
| | | * @param jeProperties The JE environment properties to parse, |
| | | * validate and set. |
| | | * @param configAttrMap Component supported JE properties to |
| | | * their configuration attributes map. |
| | | * @return An environment config instance with given properties |
| | | * set. |
| | | * @throws ConfigException If there is an error while parsing, |
| | | * validating and setting any of the properties provided. |
| | | */ |
| | | public static EnvironmentConfig setJEProperties(EnvironmentConfig envConfig, |
| | | SortedSet<String> jeProperties, HashMap<String, String> configAttrMap) |
| | | throws ConfigException |
| | | { |
| | | if (jeProperties.isEmpty()) { |
| | | // return default config. |
| | | return envConfig; |
| | | } |
| | | |
| | | // Set to catch duplicate properties. |
| | | HashSet<String> uniqueJEProperties = new HashSet<String>(); |
| | | |
| | | // Iterate through the config values associated with a JE property. |
| | | for (String jeEntry : jeProperties) |
| | | { |
| | | StringTokenizer st = new StringTokenizer(jeEntry, "="); |
| | | if (st.countTokens() == 2) { |
| | | String jePropertyName = st.nextToken(); |
| | | String jePropertyValue = st.nextToken(); |
| | | // Check if it is a duplicate. |
| | | if (uniqueJEProperties.contains(jePropertyName)) { |
| | | LocalizableMessage message = ERR_CONFIG_JE_DUPLICATE_PROPERTY.get( |
| | | jePropertyName); |
| | | throw new ConfigException(message); |
| | | } |
| | | // Set JE property. |
| | | try { |
| | | envConfig.setConfigParam(jePropertyName, jePropertyValue); |
| | | // If this property shadows an existing config attribute. |
| | | if (configAttrMap.containsKey(jePropertyName)) { |
| | | LocalizableMessage message = ERR_CONFIG_JE_PROPERTY_SHADOWS_CONFIG.get( |
| | | jePropertyName, attrMap.get(jePropertyName)); |
| | | throw new ConfigException(message); |
| | | } |
| | | // Add this property to unique set. |
| | | uniqueJEProperties.add(jePropertyName); |
| | | } catch(IllegalArgumentException e) { |
| | | logger.traceException(e); |
| | | LocalizableMessage message = |
| | | ERR_CONFIG_JE_PROPERTY_INVALID.get( |
| | | jeEntry, e.getMessage()); |
| | | throw new ConfigException(message, e.getCause()); |
| | | } |
| | | } else { |
| | | LocalizableMessage message = |
| | | ERR_CONFIG_JE_PROPERTY_INVALID_FORM.get(jeEntry); |
| | | throw new ConfigException(message); |
| | | } |
| | | } |
| | | |
| | | return envConfig; |
| | | } |
| | | |
| | | |
| | | |
| | | } |
| New file |
| | |
| | | /* |
| | | * CDDL HEADER START |
| | | * |
| | | * The contents of this file are subject to the terms of the |
| | | * Common Development and Distribution License, Version 1.0 only |
| | | * (the "License"). You may not use this file except in compliance |
| | | * with the License. |
| | | * |
| | | * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt |
| | | * or http://forgerock.org/license/CDDLv1.0.html. |
| | | * See the License for the specific language governing permissions |
| | | * and limitations under the License. |
| | | * |
| | | * When distributing Covered Code, include this CDDL HEADER in each |
| | | * file and include the License file at legal-notices/CDDLv1_0.txt. |
| | | * If applicable, add the following below this CDDL HEADER, with the |
| | | * fields enclosed by brackets "[]" replaced with your own identifying |
| | | * information: |
| | | * Portions Copyright [yyyy] [name of copyright owner] |
| | | * |
| | | * CDDL HEADER END |
| | | * |
| | | * |
| | | * Copyright 2006-2010 Sun Microsystems, Inc. |
| | | * Portions Copyright 2014 ForgeRock AS |
| | | */ |
| | | package org.opends.server.backends.pluggable; |
| | | |
| | | import org.forgerock.opendj.ldap.ByteSequence; |
| | | import org.forgerock.opendj.ldap.ByteString; |
| | | import org.opends.server.backends.pluggable.BackendImpl.ReadableStorage; |
| | | import org.opends.server.backends.pluggable.BackendImpl.Storage; |
| | | import org.opends.server.backends.pluggable.BackendImpl.StorageRuntimeException; |
| | | import org.opends.server.backends.pluggable.BackendImpl.TreeName; |
| | | import org.opends.server.backends.pluggable.BackendImpl.WriteableStorage; |
| | | import org.opends.server.types.DN; |
| | | |
| | | import static org.opends.server.backends.pluggable.JebFormat.*; |
| | | |
| | | /** |
| | | * This class represents the DN database, or dn2id, which has one record |
| | | * for each entry. The key is the normalized entry DN and the value |
| | | * is the entry ID. |
| | | */ |
| | | public class DN2ID extends DatabaseContainer |
| | | { |
| | | private final int prefixRDNComponents; |
| | | |
| | | /** |
| | | * Create a DN2ID instance for the DN database in a given entryContainer. |
| | | * |
| | | * @param treeName The name of the DN database. |
| | | * @param env The JE environment. |
| | | * @param entryContainer The entryContainer of the DN database. |
| | | * @throws StorageRuntimeException If an error occurs in the JE database. |
| | | */ |
| | | DN2ID(TreeName treeName, Storage env, EntryContainer entryContainer) |
| | | throws StorageRuntimeException |
| | | { |
| | | super(treeName, env, entryContainer); |
| | | |
| | | prefixRDNComponents = entryContainer.getBaseDN().size(); |
| | | } |
| | | |
| | | /** |
| | | * Insert a new record into the DN database. |
| | | * @param txn A JE database transaction to be used for the database operation, |
| | | * or null if none. |
| | | * @param dn The entry DN, which is the key to the record. |
| | | * @param id The entry ID, which is the value of the record. |
| | | * @return true if the record was inserted, false if a record with that key |
| | | * already exists. |
| | | * @throws StorageRuntimeException If an error occurred while attempting to insert |
| | | * the new record. |
| | | */ |
| | | public boolean insert(WriteableStorage txn, DN dn, EntryID id) throws StorageRuntimeException |
| | | { |
| | | ByteString key = dnToDNKey(dn, prefixRDNComponents); |
| | | ByteString value = id.toByteString(); |
| | | |
| | | return insert(txn, key, value); |
| | | } |
| | | |
| | | /** |
| | | * Write a record to the DN database. If a record with the given key already |
| | | * exists, the record will be replaced, otherwise a new record will be |
| | | * inserted. |
| | | * @param txn A JE database transaction to be used for the database operation, |
| | | * or null if none. |
| | | * @param dn The entry DN, which is the key to the record. |
| | | * @param id The entry ID, which is the value of the record. |
| | | * @throws StorageRuntimeException If an error occurred while attempting to write |
| | | * the record. |
| | | */ |
| | | public void put(WriteableStorage txn, DN dn, EntryID id) throws StorageRuntimeException |
| | | { |
| | | ByteString key = dnToDNKey(dn, prefixRDNComponents); |
| | | ByteString value = id.toByteString(); |
| | | |
| | | put(txn, key, value); |
| | | } |
| | | |
| | | /** |
| | | * Write a record to the DN database, where the key and value are already |
| | | * formatted. |
| | | * |
| | | * @param txn |
| | | * A JE database transaction to be used for the database operation, |
| | | * or null if none. |
| | | * @param key |
| | | * A ByteString containing the record key. |
| | | * @param value |
| | | * A ByteString containing the record value. |
| | | * @throws StorageRuntimeException |
| | | * If an error occurred while attempting to write the record. |
| | | */ |
| | | @Override |
| | | public void put(WriteableStorage txn, ByteSequence key, ByteSequence value) throws StorageRuntimeException |
| | | { |
| | | super.put(txn, key, value); |
| | | } |
| | | |
| | | /** |
| | | * Remove a record from the DN database. |
| | | * @param txn A JE database transaction to be used for the database operation, |
| | | * or null if none. |
| | | * @param dn The entry DN, which is the key to the record. |
| | | * @return true if the record was removed, false if it was not removed. |
| | | * @throws StorageRuntimeException If an error occurred while attempting to remove |
| | | * the record. |
| | | */ |
| | | public boolean remove(WriteableStorage txn, DN dn) throws StorageRuntimeException |
| | | { |
| | | ByteString key = dnToDNKey(dn, prefixRDNComponents); |
| | | |
| | | return delete(txn, key); |
| | | } |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | protected boolean delete(WriteableStorage txn, ByteSequence key) throws StorageRuntimeException |
| | | { |
| | | return super.delete(txn, key); |
| | | } |
| | | |
| | | /** |
| | | * Fetch the entry ID for a given DN. |
| | | * @param txn A JE database transaction to be used for the database read, or |
| | | * null if none is required. |
| | | * @param dn The DN for which the entry ID is desired. |
| | | * @param isRMW |
| | | * @return The entry ID, or null if the given DN is not in the DN database. |
| | | * @throws StorageRuntimeException If an error occurs in the JE database. |
| | | */ |
| | | public EntryID get(ReadableStorage txn, DN dn, boolean isRMW) throws StorageRuntimeException |
| | | { |
| | | ByteString key = dnToDNKey(dn, prefixRDNComponents); |
| | | ByteString value = read(txn, key, isRMW); |
| | | if (value != null) |
| | | { |
| | | return new EntryID(value); |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public ByteString read(ReadableStorage txn, ByteSequence key, boolean isRMW) |
| | | { |
| | | return super.read(txn, key, isRMW); |
| | | } |
| | | } |
| New file |
| | |
| | | /* |
| | | * CDDL HEADER START |
| | | * |
| | | * The contents of this file are subject to the terms of the |
| | | * Common Development and Distribution License, Version 1.0 only |
| | | * (the "License"). You may not use this file except in compliance |
| | | * with the License. |
| | | * |
| | | * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt |
| | | * or http://forgerock.org/license/CDDLv1.0.html. |
| | | * See the License for the specific language governing permissions |
| | | * and limitations under the License. |
| | | * |
| | | * When distributing Covered Code, include this CDDL HEADER in each |
| | | * file and include the License file at legal-notices/CDDLv1_0.txt. |
| | | * If applicable, add the following below this CDDL HEADER, with the |
| | | * fields enclosed by brackets "[]" replaced with your own identifying |
| | | * information: |
| | | * Portions Copyright [yyyy] [name of copyright owner] |
| | | * |
| | | * CDDL HEADER END |
| | | * |
| | | * |
| | | * Copyright 2006-2010 Sun Microsystems, Inc. |
| | | * Portions Copyright 2012-2014 ForgeRock AS |
| | | */ |
| | | package org.opends.server.backends.pluggable; |
| | | |
| | | import java.util.ArrayList; |
| | | import java.util.Collection; |
| | | import java.util.List; |
| | | import java.util.Set; |
| | | |
| | | import org.forgerock.i18n.LocalizableMessage; |
| | | import org.forgerock.i18n.slf4j.LocalizedLogger; |
| | | import org.forgerock.opendj.ldap.ByteSequence; |
| | | import org.forgerock.opendj.ldap.ByteSequenceReader; |
| | | import org.forgerock.opendj.ldap.ByteString; |
| | | import org.forgerock.opendj.ldap.ByteStringBuilder; |
| | | import org.forgerock.opendj.ldap.ConditionResult; |
| | | import org.forgerock.opendj.ldap.ResultCode; |
| | | import org.forgerock.opendj.ldap.SearchScope; |
| | | import org.opends.server.backends.pluggable.BackendImpl.Cursor; |
| | | import org.opends.server.backends.pluggable.BackendImpl.ReadableStorage; |
| | | import org.opends.server.backends.pluggable.BackendImpl.Storage; |
| | | import org.opends.server.backends.pluggable.BackendImpl.StorageRuntimeException; |
| | | import org.opends.server.backends.pluggable.BackendImpl.TreeName; |
| | | import org.opends.server.backends.pluggable.BackendImpl.WriteableStorage; |
| | | import org.opends.server.core.DirectoryServer; |
| | | import org.opends.server.core.SearchOperation; |
| | | import org.opends.server.types.Attribute; |
| | | import org.opends.server.types.AttributeType; |
| | | import org.opends.server.types.DN; |
| | | import org.opends.server.types.DirectoryException; |
| | | import org.opends.server.types.Entry; |
| | | import org.opends.server.types.LDAPURL; |
| | | import org.opends.server.types.Modification; |
| | | import org.opends.server.types.SearchResultReference; |
| | | import org.opends.server.util.StaticUtils; |
| | | |
| | | import static org.opends.messages.JebMessages.*; |
| | | import static org.opends.server.util.ServerConstants.*; |
| | | |
| | | /** |
| | | * This class represents the referral database which contains URIs from referral |
| | | * entries. The key is the DN of the referral entry and the value is that of a |
| | | * labeled URI in the ref attribute for that entry. Duplicate keys are permitted |
| | | * since a referral entry can contain multiple values of the ref attribute. Key |
| | | * order is the same as in the DN database so that all referrals in a subtree |
| | | * can be retrieved by cursoring through a range of the records. |
| | | */ |
| | | public class DN2URI extends DatabaseContainer |
| | | { |
| | | private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); |
| | | |
| | | private final int prefixRDNComponents; |
| | | |
| | | /** |
| | | * The standard attribute type that is used to specify the set of referral |
| | | * URLs in a referral entry. |
| | | */ |
| | | private final AttributeType referralType = |
| | | DirectoryServer.getAttributeType(ATTR_REFERRAL_URL); |
| | | |
| | | /** |
| | | * A flag that indicates whether there are any referrals contained in this |
| | | * database. It should only be set to {@code false} when it is known that |
| | | * there are no referrals. |
| | | */ |
| | | private volatile ConditionResult containsReferrals = |
| | | ConditionResult.UNDEFINED; |
| | | |
| | | |
| | | /** |
| | | * Create a new object representing a referral database in a given |
| | | * entryContainer. |
| | | * |
| | | * @param treeName |
| | | * The name of the referral database. |
| | | * @param storage |
| | | * The JE environment. |
| | | * @param entryContainer |
| | | * The entryContainer of the DN database. |
| | | * @throws StorageRuntimeException |
| | | * If an error occurs in the JE database. |
| | | */ |
| | | @SuppressWarnings("unchecked") |
| | | DN2URI(TreeName treeName, Storage storage, EntryContainer entryContainer) |
| | | throws StorageRuntimeException |
| | | { |
| | | super(treeName, storage, entryContainer); |
| | | |
| | | prefixRDNComponents = entryContainer.getBaseDN().size(); |
| | | } |
| | | |
| | | private ByteSequence encode(Collection<String> col) |
| | | { |
| | | if (col != null) |
| | | { |
| | | ByteStringBuilder b = new ByteStringBuilder(); |
| | | b.append(col.size()); |
| | | for (String s : col) |
| | | { |
| | | byte[] bytes = StaticUtils.getBytes(s); |
| | | b.append(bytes.length); |
| | | b.append(bytes); |
| | | } |
| | | return b; |
| | | } |
| | | return ByteString.empty(); |
| | | } |
| | | |
| | | private Collection<String> decode(ByteSequence bs) |
| | | { |
| | | if (!bs.isEmpty()) |
| | | { |
| | | ByteSequenceReader r = bs.asReader(); |
| | | final int nbElems = r.getInt(); |
| | | ArrayList<String> results = new ArrayList<String>(nbElems); |
| | | for (int i = 0; i < nbElems; i++) |
| | | { |
| | | final int stringLength = r.getInt(); |
| | | results.add(r.getString(stringLength)); |
| | | } |
| | | return results; |
| | | } |
| | | return new ArrayList<String>(); |
| | | } |
| | | |
| | | /** |
| | | * Insert a URI value in the referral database. |
| | | * |
| | | * @param txn A database transaction used for the update, or null if none is |
| | | * required. |
| | | * @param dn The DN of the referral entry. |
| | | * @param labeledURIs The labeled URI value of the ref attribute. |
| | | * @throws StorageRuntimeException If an error occurs in the JE database. |
| | | */ |
| | | private void insert(WriteableStorage txn, DN dn, Collection<String> labeledURIs) throws StorageRuntimeException |
| | | { |
| | | ByteString key = toKey(dn); |
| | | |
| | | ByteString oldValue = read(txn, key, true); |
| | | if (oldValue != null) |
| | | { |
| | | final Collection<String> newUris = decode(oldValue); |
| | | if (newUris.addAll(labeledURIs)) |
| | | { |
| | | put(txn, key, encode(newUris)); |
| | | } |
| | | } |
| | | else |
| | | { |
| | | txn.putIfAbsent(treeName, key, encode(labeledURIs)); |
| | | } |
| | | containsReferrals = ConditionResult.TRUE; |
| | | } |
| | | |
| | | /** |
| | | * Delete URI values for a given referral entry from the referral database. |
| | | * |
| | | * @param txn A database transaction used for the update, or null if none is |
| | | * required. |
| | | * @param dn The DN of the referral entry for which URI values are to be |
| | | * deleted. |
| | | * @return true if the values were deleted, false if not. |
| | | * @throws StorageRuntimeException If an error occurs in the JE database. |
| | | */ |
| | | public boolean delete(WriteableStorage txn, DN dn) throws StorageRuntimeException |
| | | { |
| | | ByteString key = toKey(dn); |
| | | |
| | | if (delete(txn, key)) |
| | | { |
| | | containsReferrals = containsReferrals(txn); |
| | | return true; |
| | | } |
| | | return false; |
| | | } |
| | | |
| | | /** |
| | | * Delete a single URI value from the referral database. |
| | | * @param txn A database transaction used for the update, or null if none is |
| | | * required. |
| | | * @param dn The DN of the referral entry. |
| | | * @param labeledURIs The URI value to be deleted. |
| | | * @return true if the value was deleted, false if not. |
| | | * @throws StorageRuntimeException If an error occurs in the JE database. |
| | | */ |
| | | public boolean delete(WriteableStorage txn, DN dn, Collection<String> labeledURIs) |
| | | throws StorageRuntimeException |
| | | { |
| | | ByteString key = toKey(dn); |
| | | |
| | | ByteString oldValue = read(txn, key, true); |
| | | if (oldValue != null) |
| | | { |
| | | final Collection<String> oldUris = decode(oldValue); |
| | | if (oldUris.removeAll(labeledURIs)) |
| | | { |
| | | put(txn, key, encode(oldUris)); |
| | | containsReferrals = containsReferrals(txn); |
| | | return true; |
| | | } |
| | | } |
| | | return false; |
| | | } |
| | | |
| | | /** |
| | | * Indicates whether the underlying database contains any referrals. |
| | | * |
| | | * @param txn The transaction to use when making the determination. |
| | | * |
| | | * @return {@code true} if it is believed that the underlying database may |
| | | * contain at least one referral, or {@code false} if it is certain |
| | | * that it doesn't. |
| | | */ |
| | | private ConditionResult containsReferrals(ReadableStorage txn) |
| | | { |
| | | Cursor cursor = txn.openCursor(treeName); |
| | | try |
| | | { |
| | | return ConditionResult.valueOf(cursor.next()); |
| | | } |
| | | catch (Exception e) |
| | | { |
| | | logger.traceException(e); |
| | | |
| | | return ConditionResult.UNDEFINED; |
| | | } |
| | | finally |
| | | { |
| | | cursor.close(); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Update the referral database for an entry that has been modified. Does |
| | | * not do anything unless the entry before the modification or the entry after |
| | | * the modification is a referral entry. |
| | | * |
| | | * @param txn A database transaction used for the update, or null if none is |
| | | * required. |
| | | * @param before The entry before the modifications have been applied. |
| | | * @param after The entry after the modifications have been applied. |
| | | * @param mods The sequence of modifications made to the entry. |
| | | * @throws StorageRuntimeException If an error occurs in the JE database. |
| | | */ |
| | | public void modifyEntry(WriteableStorage txn, Entry before, Entry after, |
| | | List<Modification> mods) |
| | | throws StorageRuntimeException |
| | | { |
| | | DN entryDN = before.getName(); |
| | | for (Modification mod : mods) |
| | | { |
| | | Attribute modAttr = mod.getAttribute(); |
| | | AttributeType modAttrType = modAttr.getAttributeType(); |
| | | if (modAttrType.equals(referralType)) |
| | | { |
| | | Attribute a = mod.getAttribute(); |
| | | switch (mod.getModificationType().asEnum()) |
| | | { |
| | | case ADD: |
| | | if (a != null) |
| | | { |
| | | insert(txn, entryDN, toStrings(a)); |
| | | } |
| | | break; |
| | | |
| | | case DELETE: |
| | | if (a == null || a.isEmpty()) |
| | | { |
| | | delete(txn, entryDN); |
| | | } |
| | | else |
| | | { |
| | | delete(txn, entryDN, toStrings(a)); |
| | | } |
| | | break; |
| | | |
| | | case INCREMENT: |
| | | // Nonsensical. |
| | | break; |
| | | |
| | | case REPLACE: |
| | | delete(txn, entryDN); |
| | | if (a != null) |
| | | { |
| | | insert(txn, entryDN, toStrings(a)); |
| | | } |
| | | break; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | private List<String> toStrings(Attribute a) |
| | | { |
| | | List<String> results = new ArrayList<String>(a.size()); |
| | | for (ByteString v : a) |
| | | { |
| | | results.add(v.toString()); |
| | | } |
| | | return results; |
| | | } |
| | | |
| | | /** |
| | | * Update the referral database for an entry that has been replaced. Does not |
| | | * do anything unless the entry before it was replaced or the entry after it |
| | | * was replaced is a referral entry. |
| | | * |
| | | * @param txn |
| | | * A database transaction used for the update, or null if none is |
| | | * required. |
| | | * @param before |
| | | * The entry before it was replaced. |
| | | * @param after |
| | | * The entry after it was replaced. |
| | | * @throws StorageRuntimeException |
| | | * If an error occurs in the JE database. |
| | | */ |
| | | public void replaceEntry(WriteableStorage txn, Entry before, Entry after) |
| | | throws StorageRuntimeException |
| | | { |
| | | deleteEntry(txn, before); |
| | | addEntry(txn, after); |
| | | } |
| | | |
| | | /** |
| | | * Update the referral database for a new entry. Does nothing if the entry |
| | | * is not a referral entry. |
| | | * @param txn A database transaction used for the update, or null if none is |
| | | * required. |
| | | * @param entry The entry to be added. |
| | | * @return True if the entry was added successfully or False otherwise. |
| | | * @throws StorageRuntimeException If an error occurs in the JE database. |
| | | */ |
| | | public boolean addEntry(WriteableStorage txn, Entry entry) |
| | | throws StorageRuntimeException |
| | | { |
| | | Set<String> labeledURIs = entry.getReferralURLs(); |
| | | if (labeledURIs != null) |
| | | { |
| | | insert(txn, entry.getName(), labeledURIs); |
| | | } |
| | | return true; |
| | | } |
| | | |
| | | /** |
| | | * Update the referral database for a deleted entry. Does nothing if the entry |
| | | * was not a referral entry. |
| | | * @param txn A database transaction used for the update, or null if none is |
| | | * required. |
| | | * @param entry The entry to be deleted. |
| | | * @throws StorageRuntimeException If an error occurs in the JE database. |
| | | */ |
| | | public void deleteEntry(WriteableStorage txn, Entry entry) |
| | | throws StorageRuntimeException |
| | | { |
| | | Set<String> labeledURIs = entry.getReferralURLs(); |
| | | if (labeledURIs != null) |
| | | { |
| | | delete(txn, entry.getName()); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Checks whether the target of an operation is a referral entry and throws |
| | | * a Directory referral exception if it is. |
| | | * @param entry The target entry of the operation, or the base entry of a |
| | | * search operation. |
| | | * @param searchScope The scope of the search operation, or null if the |
| | | * operation is not a search operation. |
| | | * @throws DirectoryException If a referral is found at or above the target |
| | | * DN. The referral URLs will be set appropriately for the references found |
| | | * in the referral entry. |
| | | */ |
| | | public void checkTargetForReferral(Entry entry, SearchScope searchScope) |
| | | throws DirectoryException |
| | | { |
| | | Set<String> referralURLs = entry.getReferralURLs(); |
| | | if (referralURLs != null) |
| | | { |
| | | throwReferralException(entry.getName(), entry.getName(), referralURLs, |
| | | searchScope); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Throws a Directory referral exception for the case where a referral entry |
| | | * exists at or above the target DN of an operation. |
| | | * @param targetDN The target DN of the operation, or the base object of a |
| | | * search operation. |
| | | * @param referralDN The DN of the referral entry. |
| | | * @param labeledURIs The set of labeled URIs in the referral entry. |
| | | * @param searchScope The scope of the search operation, or null if the |
| | | * operation is not a search operation. |
| | | * @throws DirectoryException If a referral is found at or above the target |
| | | * DN. The referral URLs will be set appropriately for the references found |
| | | * in the referral entry. |
| | | */ |
| | | public void throwReferralException(DN targetDN, DN referralDN, Collection<String> labeledURIs, SearchScope searchScope) |
| | | throws DirectoryException |
| | | { |
| | | ArrayList<String> URIList = new ArrayList<String>(labeledURIs.size()); |
| | | for (String labeledURI : labeledURIs) |
| | | { |
| | | // Remove the label part of the labeled URI if there is a label. |
| | | String uri = labeledURI; |
| | | int i = labeledURI.indexOf(' '); |
| | | if (i != -1) |
| | | { |
| | | uri = labeledURI.substring(0, i); |
| | | } |
| | | |
| | | try |
| | | { |
| | | LDAPURL ldapurl = LDAPURL.decode(uri, false); |
| | | |
| | | if ("ldap".equalsIgnoreCase(ldapurl.getScheme())) |
| | | { |
| | | DN urlBaseDN = targetDN; |
| | | if (!referralDN.equals(ldapurl.getBaseDN())) |
| | | { |
| | | urlBaseDN = |
| | | EntryContainer.modDN(targetDN, |
| | | referralDN.size(), |
| | | ldapurl.getBaseDN()); |
| | | } |
| | | ldapurl.setBaseDN(urlBaseDN); |
| | | if (searchScope == null) |
| | | { |
| | | // RFC 3296, 5.2. Target Object Considerations: |
| | | // In cases where the URI to be returned is a LDAP URL, the server |
| | | // SHOULD trim any present scope, filter, or attribute list from the |
| | | // URI before returning it. Critical extensions MUST NOT be trimmed |
| | | // or modified. |
| | | StringBuilder builder = new StringBuilder(uri.length()); |
| | | ldapurl.toString(builder, true); |
| | | uri = builder.toString(); |
| | | } |
| | | else |
| | | { |
| | | // RFC 3296, 5.3. Base Object Considerations: |
| | | // In cases where the URI to be returned is a LDAP URL, the server |
| | | // MUST provide an explicit scope specifier from the LDAP URL prior |
| | | // to returning it. |
| | | ldapurl.getAttributes().clear(); |
| | | ldapurl.setScope(searchScope); |
| | | ldapurl.setFilter(null); |
| | | uri = ldapurl.toString(); |
| | | } |
| | | } |
| | | } |
| | | catch (DirectoryException e) |
| | | { |
| | | logger.traceException(e); |
| | | // Return the non-LDAP URI as is. |
| | | } |
| | | |
| | | URIList.add(uri); |
| | | } |
| | | |
| | | // Throw a directory referral exception containing the URIs. |
| | | LocalizableMessage msg = NOTE_JEB_REFERRAL_RESULT_MESSAGE.get(referralDN); |
| | | throw new DirectoryException( |
| | | ResultCode.REFERRAL, msg, referralDN, URIList, null); |
| | | } |
| | | |
| | | /** |
| | | * Process referral entries that are above the target DN of an operation. |
| | | * @param targetDN The target DN of the operation, or the base object of a |
| | | * search operation. |
| | | * @param searchScope The scope of the search operation, or null if the |
| | | * operation is not a search operation. |
| | | * @throws DirectoryException If a referral is found at or above the target |
| | | * DN. The referral URLs will be set appropriately for the references found |
| | | * in the referral entry. |
| | | */ |
| | | public void targetEntryReferrals(DN targetDN, SearchScope searchScope) |
| | | throws DirectoryException |
| | | { |
| | | if (containsReferrals == ConditionResult.UNDEFINED) |
| | | { |
| | | containsReferrals = containsReferrals(null); |
| | | } |
| | | |
| | | if (containsReferrals == ConditionResult.FALSE) |
| | | { |
| | | return; |
| | | } |
| | | |
| | | try |
| | | { |
| | | Cursor cursor = storage.openCursor(treeName); |
| | | try |
| | | { |
| | | // Go up through the DIT hierarchy until we find a referral. |
| | | for (DN dn = entryContainer.getParentWithinBase(targetDN); dn != null; |
| | | dn = entryContainer.getParentWithinBase(dn)) |
| | | { |
| | | // Look for a record whose key matches the current DN. |
| | | if (cursor.positionToKey(toKey(dn))) |
| | | { |
| | | // Construct a set of all the labeled URIs in the referral. |
| | | Collection<String> labeledURIs = decode(cursor.getValue()); |
| | | throwReferralException(targetDN, dn, labeledURIs, searchScope); |
| | | } |
| | | } |
| | | } |
| | | finally |
| | | { |
| | | cursor.close(); |
| | | } |
| | | } |
| | | catch (StorageRuntimeException e) |
| | | { |
| | | logger.traceException(e); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Return search result references for a search operation using the referral |
| | | * database to find all referral entries within scope of the search. |
| | | * @param searchOp The search operation for which search result references |
| | | * should be returned. |
| | | * @return <CODE>true</CODE> if the caller should continue processing the |
| | | * search request and sending additional entries and references, or |
| | | * <CODE>false</CODE> if not for some reason (e.g., the size limit |
| | | * has been reached or the search has been abandoned). |
| | | * @throws DirectoryException If a Directory Server error occurs. |
| | | */ |
| | | public boolean returnSearchReferences(SearchOperation searchOp) |
| | | throws DirectoryException |
| | | { |
| | | if (containsReferrals == ConditionResult.UNDEFINED) |
| | | { |
| | | containsReferrals = containsReferrals(null); |
| | | } |
| | | |
| | | if (containsReferrals == ConditionResult.FALSE) |
| | | { |
| | | return true; |
| | | } |
| | | |
| | | /* |
| | | * We will iterate forwards through a range of the keys to |
| | | * find subordinates of the base entry from the top of the tree downwards. |
| | | */ |
| | | ByteString baseDN = toKey(searchOp.getBaseDN()); |
| | | ByteStringBuilder suffix = new ByteStringBuilder(baseDN.length() + 1); |
| | | suffix.append(baseDN); |
| | | ByteStringBuilder end = new ByteStringBuilder(suffix); |
| | | |
| | | /* |
| | | * Set the ending value to a value of equal length but slightly |
| | | * greater than the suffix. Since keys are compared in |
| | | * reverse order we must set the first byte (the comma). |
| | | * No possibility of overflow here. |
| | | */ |
| | | suffix.append((byte) 0x00); |
| | | end.append((byte) 0x01); |
| | | |
| | | ByteSequence startKey = suffix; |
| | | try |
| | | { |
| | | Cursor cursor = storage.openCursor(treeName); |
| | | try |
| | | { |
| | | // Initialize the cursor very close to the starting value then |
| | | // step forward until we pass the ending value. |
| | | boolean success = cursor.positionToKey(startKey); |
| | | while (success) |
| | | { |
| | | ByteString key = cursor.getKey(); |
| | | int cmp = ByteSequence.COMPARATOR.compare(key, end); |
| | | if (cmp >= 0) |
| | | { |
| | | // We have gone past the ending value. |
| | | break; |
| | | } |
| | | |
| | | // We have found a subordinate referral. |
| | | DN dn = JebFormat.dnFromDNKey(key, entryContainer.getBaseDN()); |
| | | |
| | | // Make sure the referral is within scope. |
| | | if (searchOp.getScope() == SearchScope.SINGLE_LEVEL |
| | | && JebFormat.findDNKeyParent(key) != baseDN.length()) |
| | | { |
| | | continue; |
| | | } |
| | | |
| | | // Construct a list of all the URIs in the referral. |
| | | Collection<String> labeledURIs = decode(cursor.getValue()); |
| | | SearchResultReference reference = toSearchResultReference(dn, labeledURIs, searchOp.getScope()); |
| | | if (!searchOp.returnReference(dn, reference)) |
| | | { |
| | | return false; |
| | | } |
| | | success = cursor.next(); |
| | | } |
| | | } |
| | | finally |
| | | { |
| | | cursor.close(); |
| | | } |
| | | } |
| | | catch (StorageRuntimeException e) |
| | | { |
| | | logger.traceException(e); |
| | | } |
| | | |
| | | return true; |
| | | } |
| | | |
| | | private SearchResultReference toSearchResultReference(DN dn, Collection<String> labeledURIs, SearchScope scope) |
| | | { |
| | | ArrayList<String> URIList = new ArrayList<String>(labeledURIs.size()); |
| | | for (String labeledURI : labeledURIs) |
| | | { |
| | | // Remove the label part of the labeled URI if there is a label. |
| | | String uri = labeledURI; |
| | | int i = labeledURI.indexOf(' '); |
| | | if (i != -1) |
| | | { |
| | | uri = labeledURI.substring(0, i); |
| | | } |
| | | |
| | | // From RFC 3296 section 5.4: |
| | | // If the URI component is not a LDAP URL, it should be returned as |
| | | // is. If the LDAP URL's DN part is absent or empty, the DN part |
| | | // must be modified to contain the DN of the referral object. If |
| | | // the URI component is a LDAP URL, the URI SHOULD be modified to |
| | | // add an explicit scope specifier. |
| | | try |
| | | { |
| | | LDAPURL ldapurl = LDAPURL.decode(uri, false); |
| | | |
| | | if ("ldap".equalsIgnoreCase(ldapurl.getScheme())) |
| | | { |
| | | if (ldapurl.getBaseDN().isRootDN()) |
| | | { |
| | | ldapurl.setBaseDN(dn); |
| | | } |
| | | ldapurl.getAttributes().clear(); |
| | | if (scope == SearchScope.SINGLE_LEVEL) |
| | | { |
| | | ldapurl.setScope(SearchScope.BASE_OBJECT); |
| | | } |
| | | else |
| | | { |
| | | ldapurl.setScope(SearchScope.WHOLE_SUBTREE); |
| | | } |
| | | ldapurl.setFilter(null); |
| | | uri = ldapurl.toString(); |
| | | } |
| | | } |
| | | catch (DirectoryException e) |
| | | { |
| | | logger.traceException(e); |
| | | // Return the non-LDAP URI as is. |
| | | } |
| | | |
| | | URIList.add(uri); |
| | | } |
| | | return new SearchResultReference(URIList); |
| | | } |
| | | |
| | | private ByteString toKey(DN dn) |
| | | { |
| | | return JebFormat.dnToDNKey(dn, prefixRDNComponents); |
| | | } |
| | | } |
| New file |
| | |
| | | /* |
| | | * CDDL HEADER START |
| | | * |
| | | * The contents of this file are subject to the terms of the |
| | | * Common Development and Distribution License, Version 1.0 only |
| | | * (the "License"). You may not use this file except in compliance |
| | | * with the License. |
| | | * |
| | | * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt |
| | | * or http://forgerock.org/license/CDDLv1.0.html. |
| | | * See the License for the specific language governing permissions |
| | | * and limitations under the License. |
| | | * |
| | | * When distributing Covered Code, include this CDDL HEADER in each |
| | | * file and include the License file at legal-notices/CDDLv1_0.txt. |
| | | * If applicable, add the following below this CDDL HEADER, with the |
| | | * fields enclosed by brackets "[]" replaced with your own identifying |
| | | * information: |
| | | * Portions Copyright [yyyy] [name of copyright owner] |
| | | * |
| | | * CDDL HEADER END |
| | | * |
| | | * |
| | | * Copyright 2006-2008 Sun Microsystems, Inc. |
| | | * Portions Copyright 2014 ForgeRock AS |
| | | */ |
| | | package org.opends.server.backends.pluggable; |
| | | |
| | | import org.forgerock.util.Reject; |
| | | import org.opends.server.api.CompressedSchema; |
| | | import org.opends.server.types.EntryEncodeConfig; |
| | | |
| | | /** |
| | | * Configuration class to indicate desired compression and cryptographic options |
| | | * for the data stored in the database. |
| | | */ |
| | | public final class DataConfig |
| | | { |
| | | /** Indicates whether data should be compressed before writing to the database. */ |
| | | private boolean compressed; |
| | | |
| | | /** The configuration to use when encoding entries in the database. */ |
| | | private EntryEncodeConfig encodeConfig = new EntryEncodeConfig(); |
| | | |
| | | /** |
| | | * Construct a new DataConfig object with the specified settings. |
| | | * |
| | | * @param compressed true if data should be compressed, false if not. |
| | | * @param compactEncoding true if data should be encoded in compact form, |
| | | * false if not. |
| | | * @param compressedSchema the compressed schema manager to use. It must not |
| | | * be {@code null} if compactEncoding is {@code true}. |
| | | */ |
| | | public DataConfig(boolean compressed, boolean compactEncoding, CompressedSchema compressedSchema) |
| | | { |
| | | this.compressed = compressed; |
| | | setCompactEncoding(compactEncoding, compressedSchema); |
| | | } |
| | | |
| | | /** |
| | | * Determine whether data should be compressed before writing to the database. |
| | | * @return true if data should be compressed, false if not. |
| | | */ |
| | | public boolean isCompressed() |
| | | { |
| | | return compressed; |
| | | } |
| | | |
| | | /** |
| | | * Determine whether entries should be encoded with the compact form before |
| | | * writing to the database. |
| | | * @return true if data should be encoded in the compact form. |
| | | */ |
| | | public boolean isCompactEncoding() |
| | | { |
| | | return encodeConfig.compressAttributeDescriptions(); |
| | | } |
| | | |
| | | /** |
| | | * Configure whether data should be compressed before writing to the database. |
| | | * @param compressed true if data should be compressed, false if not. |
| | | */ |
| | | public void setCompressed(boolean compressed) |
| | | { |
| | | this.compressed = compressed; |
| | | } |
| | | |
| | | /** |
| | | * Configure whether data should be encoded with the compact form before |
| | | * writing to the database. |
| | | * @param compactEncoding true if data should be encoded in compact form, |
| | | * false if not. |
| | | * @param compressedSchema The compressed schema manager to use. It must not |
| | | * be {@code null} if compactEncoding is {@code true}. |
| | | */ |
| | | public void setCompactEncoding(boolean compactEncoding, CompressedSchema compressedSchema) |
| | | { |
| | | if (compressedSchema == null) |
| | | { |
| | | Reject.ifTrue(compactEncoding); |
| | | this.encodeConfig = new EntryEncodeConfig(false, compactEncoding, false); |
| | | } |
| | | else |
| | | { |
| | | this.encodeConfig = new EntryEncodeConfig(false, compactEncoding, compactEncoding, compressedSchema); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Get the EntryEncodeConfig object in use by this configuration. |
| | | * @return the EntryEncodeConfig object in use by this configuration. |
| | | */ |
| | | public EntryEncodeConfig getEntryEncodeConfig() |
| | | { |
| | | return this.encodeConfig; |
| | | } |
| | | |
| | | /** |
| | | * Get a string representation of this object. |
| | | * @return A string representation of this object. |
| | | */ |
| | | @Override |
| | | public String toString() |
| | | { |
| | | final StringBuilder builder = new StringBuilder(); |
| | | builder.append("DataConfig(compressed="); |
| | | builder.append(compressed); |
| | | builder.append(", "); |
| | | encodeConfig.toString(builder); |
| | | builder.append(")"); |
| | | return builder.toString(); |
| | | } |
| | | } |
| New file |
| | |
| | | /* |
| | | * CDDL HEADER START |
| | | * |
| | | * The contents of this file are subject to the terms of the |
| | | * Common Development and Distribution License, Version 1.0 only |
| | | * (the "License"). You may not use this file except in compliance |
| | | * with the License. |
| | | * |
| | | * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt |
| | | * or http://forgerock.org/license/CDDLv1.0.html. |
| | | * See the License for the specific language governing permissions |
| | | * and limitations under the License. |
| | | * |
| | | * When distributing Covered Code, include this CDDL HEADER in each |
| | | * file and include the License file at legal-notices/CDDLv1_0.txt. |
| | | * If applicable, add the following below this CDDL HEADER, with the |
| | | * fields enclosed by brackets "[]" replaced with your own identifying |
| | | * information: |
| | | * Portions Copyright [yyyy] [name of copyright owner] |
| | | * |
| | | * CDDL HEADER END |
| | | * |
| | | * |
| | | * Copyright 2006-2008 Sun Microsystems, Inc. |
| | | * Portions Copyright 2011-2014 ForgeRock AS |
| | | */ |
| | | package org.opends.server.backends.pluggable; |
| | | |
| | | import java.io.Closeable; |
| | | |
| | | import org.forgerock.i18n.slf4j.LocalizedLogger; |
| | | import org.forgerock.opendj.ldap.ByteSequence; |
| | | import org.forgerock.opendj.ldap.ByteString; |
| | | import org.opends.server.backends.pluggable.BackendImpl.Cursor; |
| | | import org.opends.server.backends.pluggable.BackendImpl.ReadableStorage; |
| | | import org.opends.server.backends.pluggable.BackendImpl.Storage; |
| | | import org.opends.server.backends.pluggable.BackendImpl.StorageRuntimeException; |
| | | import org.opends.server.backends.pluggable.BackendImpl.TreeName; |
| | | import org.opends.server.backends.pluggable.BackendImpl.WriteableStorage; |
| | | import org.opends.server.util.ServerConstants; |
| | | import org.opends.server.util.StaticUtils; |
| | | |
| | | /** |
| | | * This class is a wrapper around the JE database object and provides basic |
| | | * read and write methods for entries. |
| | | */ |
| | | public abstract class DatabaseContainer implements Closeable |
| | | { |
| | | private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); |
| | | |
| | | /** The database entryContainer. */ |
| | | protected final EntryContainer entryContainer; |
| | | /** The name of the database within the entryContainer. */ |
| | | protected TreeName treeName; |
| | | |
| | | /** The reference to the JE Storage. */ |
| | | protected final Storage storage; |
| | | |
| | | /** |
| | | * Create a new DatabaseContainer object. |
| | | * |
| | | * @param treeName The name of the entry database. |
| | | * @param storage The JE Storage. |
| | | * @param entryContainer The entryContainer of the entry database. |
| | | */ |
| | | protected DatabaseContainer(TreeName treeName, Storage storage, EntryContainer entryContainer) |
| | | { |
| | | this.storage = storage; |
| | | this.entryContainer = entryContainer; |
| | | this.treeName = treeName; |
| | | } |
| | | |
| | | /** |
| | | * Opens a JE database in this database container. If the provided |
| | | * database configuration is transactional, a transaction will be |
| | | * created and used to perform the open. |
| | | * |
| | | * @throws StorageRuntimeException if a JE database error occurs while |
| | | * opening the index. |
| | | */ |
| | | public void open() throws StorageRuntimeException |
| | | { |
| | | if (dbConfig.getTransactional()) |
| | | { |
| | | // Open the database under a transaction. |
| | | Transaction txn = entryContainer.beginTransaction(); |
| | | try |
| | | { |
| | | treeName = storage.openDatabase(txn, treeName, dbConfig); |
| | | if (logger.isTraceEnabled()) |
| | | { |
| | | logger.trace("JE database %s opened. txnid=%d", treeName, txn.getId()); |
| | | } |
| | | EntryContainer.transactionCommit(txn); |
| | | } |
| | | catch (StorageRuntimeException e) |
| | | { |
| | | EntryContainer.transactionAbort(txn); |
| | | throw e; |
| | | } |
| | | } |
| | | else |
| | | { |
| | | treeName = storage.openDatabase(null, treeName, dbConfig); |
| | | if (logger.isTraceEnabled()) |
| | | { |
| | | logger.trace("JE database %s opened. txnid=none", treeName); |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Flush any cached database information to disk and close the |
| | | * database container. |
| | | * |
| | | * The database container should not be closed while other processes |
| | | * acquired the container. The container should not be closed |
| | | * while cursors handles into the database remain open, or |
| | | * transactions that include operations on the database have not yet |
| | | * been committed or aborted. |
| | | * |
| | | * The container may not be accessed again after this method is |
| | | * called, regardless of the method's success or failure. |
| | | * |
| | | * @throws StorageRuntimeException if an error occurs. |
| | | */ |
| | | @Override |
| | | public synchronized void close() throws StorageRuntimeException |
| | | { |
| | | if(dbConfig.getDeferredWrite()) |
| | | { |
| | | treeName.sync(); |
| | | } |
| | | storage.openTree(treeName) |
| | | treeName.close(); |
| | | treeName = null; |
| | | |
| | | if(logger.isTraceEnabled()) |
| | | { |
| | | logger.trace("Closed tree %s", treeName); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Replace or insert a record into a JE database, with optional debug logging. |
| | | * This is a simple wrapper around the JE Database.put method. |
| | | * @param txn The JE transaction handle, or null if none. |
| | | * @param key The record key. |
| | | * @param value The record value. |
| | | * @throws StorageRuntimeException If an error occurs in the JE operation. |
| | | */ |
| | | protected void put(WriteableStorage txn, ByteSequence key, ByteSequence value) |
| | | throws StorageRuntimeException |
| | | { |
| | | txn.put(treeName, key, value); |
| | | if (logger.isTraceEnabled()) |
| | | { |
| | | logger.trace(messageToLog(true, treeName, txn, key, value)); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Read a record from a JE database, with optional debug logging. This is a |
| | | * simple wrapper around the JE Database.get method. |
| | | * @param txn The JE transaction handle, or null if none. |
| | | * @param key The key of the record to be read. |
| | | * @return The operation status. |
| | | * @throws StorageRuntimeException If an error occurs in the JE operation. |
| | | */ |
| | | protected ByteString read(ReadableStorage txn, ByteSequence key, boolean isRMW) throws StorageRuntimeException |
| | | { |
| | | ByteString value = txn.get(treeName, key); |
| | | if (logger.isTraceEnabled()) |
| | | { |
| | | logger.trace(messageToLog(value != null, treeName, txn, key, value)); |
| | | } |
| | | return value; |
| | | } |
| | | |
| | | /** |
| | | * Insert a record into a JE database, with optional debug logging. This is a |
| | | * simple wrapper around the JE Database.putNoOverwrite method. |
| | | * @param txn The JE transaction handle, or null if none. |
| | | * @param key The record key. |
| | | * @param value The record value. |
| | | * @return <code>true</code> if the key-value mapping could be inserted, <code>false</code> if the key was already mapped to another value |
| | | * @throws StorageRuntimeException If an error occurs in the JE operation. |
| | | */ |
| | | protected boolean insert(WriteableStorage txn, ByteString key, ByteString value) throws StorageRuntimeException |
| | | { |
| | | boolean result = txn.putIfAbsent(treeName, key, value); |
| | | if (logger.isTraceEnabled()) |
| | | { |
| | | logger.trace(messageToLog(result, treeName, txn, key, value)); |
| | | } |
| | | return result; |
| | | } |
| | | |
| | | /** |
| | | * Delete a record from a JE database, with optional debug logging. This is a |
| | | * simple wrapper around the JE Database.delete method. |
| | | * @param txn The JE transaction handle, or null if none. |
| | | * @param key The key of the record to be read. |
| | | * @return <code>true</code> if the key mapping was removed, <code>false</code> otherwise |
| | | * @throws StorageRuntimeException If an error occurs in the JE operation. |
| | | */ |
| | | protected boolean delete(WriteableStorage txn, ByteSequence key) throws StorageRuntimeException |
| | | { |
| | | boolean result = txn.remove(treeName, key); |
| | | if (logger.isTraceEnabled()) |
| | | { |
| | | logger.trace(messageToLog(result, treeName, txn, key, null)); |
| | | } |
| | | return result; |
| | | } |
| | | |
| | | /** |
| | | * Open a JE cursor on the JE database. This is a simple wrapper around |
| | | * the JE Database.openCursor method. |
| | | * @param txn A JE database transaction to be used by the cursor, |
| | | * or null if none. |
| | | * @return A JE cursor. |
| | | * @throws StorageRuntimeException If an error occurs while attempting to open |
| | | * the cursor. |
| | | */ |
| | | public Cursor openCursor(ReadableStorage txn) throws StorageRuntimeException |
| | | { |
| | | return txn.openCursor(treeName); |
| | | } |
| | | |
| | | /** |
| | | * Get the count of key/data pairs in the database in a JE database. |
| | | * This is a simple wrapper around the JE Database.count method. |
| | | * @return The count of key/data pairs in the database. |
| | | * @throws StorageRuntimeException If an error occurs in the JE operation. |
| | | */ |
| | | public long getRecordCount() throws StorageRuntimeException |
| | | { |
| | | long count = treeName.count(); |
| | | if (logger.isTraceEnabled()) |
| | | { |
| | | logger.trace(messageToLog(true, treeName, null, null, null)); |
| | | } |
| | | return count; |
| | | } |
| | | |
| | | /** |
| | | * Get a string representation of this object. |
| | | * @return return A string representation of this object. |
| | | */ |
| | | @Override |
| | | public String toString() |
| | | { |
| | | return treeName.toString(); |
| | | } |
| | | |
| | | /** |
| | | * Get the JE database name for this database container. |
| | | * |
| | | * @return JE database name for this database container. |
| | | */ |
| | | public TreeName getName() |
| | | { |
| | | return treeName; |
| | | } |
| | | |
| | | /** |
| | | * Preload the database into cache. |
| | | * |
| | | * @param config The preload configuration. |
| | | * @return Statistics about the preload process. |
| | | * @throws StorageRuntimeException If an JE database error occurs |
| | | * during the preload. |
| | | */ |
| | | public PreloadStats preload(PreloadConfig config) |
| | | throws StorageRuntimeException |
| | | { |
| | | return treeName.preload(config); |
| | | } |
| | | |
| | | /** |
| | | * Set the JE database name to use for this container. |
| | | * |
| | | * @param name The database name to use for this container. |
| | | */ |
| | | void setName(TreeName name) |
| | | { |
| | | this.treeName = name; |
| | | } |
| | | |
| | | /** Returns the message to log given the provided information. */ |
| | | private String messageToLog(boolean success, TreeName treeName, ReadableStorage txn, ByteSequence key, |
| | | ByteSequence value) |
| | | { |
| | | StringBuilder builder = new StringBuilder(); |
| | | builder.append(" ("); |
| | | builder.append(success ? "SUCCESS" : "ERROR"); |
| | | builder.append(")"); |
| | | builder.append(" db="); |
| | | builder.append(treeName); |
| | | if (txn != null) |
| | | { |
| | | builder.append(" txnid="); |
| | | try |
| | | { |
| | | builder.append(txn.getId()); |
| | | } |
| | | catch (StorageRuntimeException de) |
| | | { |
| | | builder.append(de); |
| | | } |
| | | } |
| | | else |
| | | { |
| | | builder.append(" txnid=none"); |
| | | } |
| | | |
| | | builder.append(ServerConstants.EOL); |
| | | if (key != null) |
| | | { |
| | | builder.append("key:"); |
| | | builder.append(ServerConstants.EOL); |
| | | StaticUtils.byteArrayToHexPlusAscii(builder, key.toByteArray(), 4); |
| | | } |
| | | |
| | | // If the operation was successful we log the same common information |
| | | // plus the data |
| | | if (value != null) |
| | | { |
| | | builder.append("value(len="); |
| | | builder.append(value.length()); |
| | | builder.append("):"); |
| | | builder.append(ServerConstants.EOL); |
| | | StaticUtils.byteArrayToHexPlusAscii(builder, value.toByteArray(), 4); |
| | | } |
| | | return builder.toString(); |
| | | } |
| | | |
| | | } |
| New file |
| | |
| | | /* |
| | | * CDDL HEADER START |
| | | * |
| | | * The contents of this file are subject to the terms of the |
| | | * Common Development and Distribution License, Version 1.0 only |
| | | * (the "License"). You may not use this file except in compliance |
| | | * with the License. |
| | | * |
| | | * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt |
| | | * or http://forgerock.org/license/CDDLv1.0.html. |
| | | * See the License for the specific language governing permissions |
| | | * and limitations under the License. |
| | | * |
| | | * When distributing Covered Code, include this CDDL HEADER in each |
| | | * file and include the License file at legal-notices/CDDLv1_0.txt. |
| | | * If applicable, add the following below this CDDL HEADER, with the |
| | | * fields enclosed by brackets "[]" replaced with your own identifying |
| | | * information: |
| | | * Portions Copyright [yyyy] [name of copyright owner] |
| | | * |
| | | * CDDL HEADER END |
| | | * |
| | | * |
| | | * Copyright 2006-2008 Sun Microsystems, Inc. |
| | | * Portions Copyright 2014 ForgeRock AS |
| | | */ |
| | | package org.opends.server.backends.pluggable; |
| | | |
| | | import java.util.Comparator; |
| | | |
| | | import org.opends.server.backends.pluggable.BackendImpl.TreeName; |
| | | |
| | | /** |
| | | * This comparator is used to sort databases in order of priority |
| | | * for preloading into the cache. |
| | | */ |
| | | public class DbPreloadComparator |
| | | implements Comparator<DatabaseContainer> |
| | | { |
| | | |
| | | /** |
| | | * Calculate the relative priority of a database for preloading. |
| | | * |
| | | * @param database A handle to the database. |
| | | * @return 1 for id2entry database, 2 for dn2id database, 3 for all others. |
| | | */ |
| | | static private int priority(DatabaseContainer database) |
| | | { |
| | | TreeName name = database.getName(); |
| | | if (name.endsWith(EntryContainer.ID2ENTRY_DATABASE_NAME)) |
| | | { |
| | | return 1; |
| | | } |
| | | else if (name.endsWith(EntryContainer.DN2ID_DATABASE_NAME)) |
| | | { |
| | | return 2; |
| | | } |
| | | else |
| | | { |
| | | return 3; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Compares its two arguments for order. Returns a negative integer, |
| | | * zero, or a positive integer as the first argument is less than, equal |
| | | * to, or greater than the second. |
| | | * |
| | | * @param database1 the first object to be compared. |
| | | * @param database2 the second object to be compared. |
| | | * @return a negative integer, zero, or a positive integer as the |
| | | * first argument is less than, equal to, or greater than the |
| | | * second. |
| | | **/ |
| | | @Override |
| | | public int compare(DatabaseContainer database1, DatabaseContainer database2) |
| | | { |
| | | return priority(database1) - priority(database2); |
| | | } |
| | | } |
| New file |
| | |
| | | /* |
| | | * CDDL HEADER START |
| | | * |
| | | * The contents of this file are subject to the terms of the |
| | | * Common Development and Distribution License, Version 1.0 only |
| | | * (the "License"). You may not use this file except in compliance |
| | | * with the License. |
| | | * |
| | | * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt |
| | | * or http://forgerock.org/license/CDDLv1.0.html. |
| | | * See the License for the specific language governing permissions |
| | | * and limitations under the License. |
| | | * |
| | | * When distributing Covered Code, include this CDDL HEADER in each |
| | | * file and include the License file at legal-notices/CDDLv1_0.txt. |
| | | * If applicable, add the following below this CDDL HEADER, with the |
| | | * fields enclosed by brackets "[]" replaced with your own identifying |
| | | * information: |
| | | * Portions Copyright [yyyy] [name of copyright owner] |
| | | * |
| | | * CDDL HEADER END |
| | | * |
| | | * |
| | | * Copyright 2008 Sun Microsystems, Inc. |
| | | * Portions Copyright 2014 ForgeRock AS |
| | | */ |
| | | package org.opends.server.backends.pluggable; |
| | | |
| | | |
| | | import java.util.Collection; |
| | | import java.util.Collections; |
| | | import java.util.Iterator; |
| | | import java.util.LinkedList; |
| | | import java.util.List; |
| | | import java.util.Timer; |
| | | import java.util.TimerTask; |
| | | import java.util.concurrent.LinkedBlockingQueue; |
| | | import java.util.concurrent.atomic.AtomicBoolean; |
| | | import java.util.concurrent.atomic.AtomicLong; |
| | | |
| | | import org.forgerock.i18n.slf4j.LocalizedLogger; |
| | | import org.forgerock.opendj.ldap.ByteString; |
| | | import org.opends.server.api.DirectoryThread; |
| | | import org.opends.server.backends.pluggable.BackendImpl.Cursor; |
| | | import org.opends.server.core.DirectoryServer; |
| | | import org.opends.server.types.Entry; |
| | | |
| | | import static org.opends.messages.ExtensionMessages.*; |
| | | import static org.opends.server.util.StaticUtils.*; |
| | | |
| | | /** |
| | | * This class defines a utility that will be used to pre-load the Directory |
| | | * Server entry cache. Pre-loader is multi-threaded and consist of the |
| | | * following threads: |
| | | * |
| | | * - The Arbiter thread which monitors overall pre-load progress and manages |
| | | * pre-load worker threads by adding or removing them as deemed necessary. |
| | | * |
| | | * - The Collector thread which collects all entries stored within the |
| | | * backend and places them to a blocking queue workers consume from. |
| | | * |
| | | * - Worker threads which are responsible for monitoring the collector feed |
| | | * and processing the actual entries for cache storage. |
| | | * |
| | | * This implementation is self-adjusting to any system workload and does not |
| | | * require any configuration parameters to optimize for initial system |
| | | * resources availability and/or any subsequent fluctuations. |
| | | */ |
| | | class EntryCachePreloader |
| | | { |
| | | private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); |
| | | |
| | | /** |
| | | * BackendImpl object. |
| | | */ |
| | | private BackendImpl backend; |
| | | |
| | | /** |
| | | * Interrupt flag for the arbiter to terminate worker threads. |
| | | */ |
| | | private AtomicBoolean interruptFlag = new AtomicBoolean(false); |
| | | |
| | | /** |
| | | * Processed entries counter. |
| | | */ |
| | | private AtomicLong processedEntries = new AtomicLong(0); |
| | | |
| | | /** |
| | | * Progress report resolution. |
| | | */ |
| | | private static final long progressInterval = 5000; |
| | | |
| | | /** |
| | | * Default resolution time. |
| | | */ |
| | | public static final long |
| | | PRELOAD_DEFAULT_SLEEP_TIME = 10000; |
| | | |
| | | /** |
| | | * Effective synchronization time. |
| | | */ |
| | | private static long syncSleepTime; |
| | | |
| | | /** |
| | | * Default queue capacity. |
| | | */ |
| | | public static final int |
| | | PRELOAD_DEFAULT_QUEUE_CAPACITY = 128; |
| | | |
| | | /** |
| | | * Effective queue capacity. |
| | | */ |
| | | private static int queueCapacity; |
| | | |
| | | /** |
| | | * Worker threads. |
| | | */ |
| | | private List<Thread> preloadThreads = |
| | | Collections.synchronizedList( |
| | | new LinkedList<Thread>()); |
| | | |
| | | /** |
| | | * Collector thread. |
| | | */ |
| | | private EntryCacheCollector collector = |
| | | new EntryCacheCollector(); |
| | | |
| | | /** |
| | | * This queue is for workers to take from. |
| | | */ |
| | | private LinkedBlockingQueue<PreloadEntry> entryQueue; |
| | | |
| | | /** |
| | | * The number of bytes in a megabyte. |
| | | */ |
| | | private static final int bytesPerMegabyte = 1024*1024; |
| | | |
| | | /** |
| | | * Constructs the Entry Cache Pre-loader for |
| | | * a given JEB implementation instance. |
| | | * |
| | | * @param jeb The JEB instance to pre-load. |
| | | */ |
| | | public EntryCachePreloader(BackendImpl jeb) { |
| | | // These should not be exposed as configuration |
| | | // parameters and are only useful for testing. |
| | | syncSleepTime = Long.getLong( |
| | | "org.opends.server.entrycache.preload.sleep", |
| | | PRELOAD_DEFAULT_SLEEP_TIME); |
| | | queueCapacity = Integer.getInteger( |
| | | "org.opends.server.entrycache.preload.queue", |
| | | PRELOAD_DEFAULT_QUEUE_CAPACITY); |
| | | entryQueue = |
| | | new LinkedBlockingQueue<PreloadEntry>( |
| | | queueCapacity); |
| | | this.backend = jeb; |
| | | } |
| | | |
| | | /** |
| | | * The Arbiter thread. |
| | | */ |
| | | protected void preload() |
| | | { |
| | | logger.info(NOTE_CACHE_PRELOAD_PROGRESS_START, backend.getBackendID()); |
| | | // Start collector thread first. |
| | | collector.start(); |
| | | // Kick off a single worker. |
| | | EntryCachePreloadWorker singleWorkerThread = |
| | | new EntryCachePreloadWorker(); |
| | | singleWorkerThread.start(); |
| | | preloadThreads.add(singleWorkerThread); |
| | | // Progress report timer task. |
| | | Timer timer = new Timer(); |
| | | TimerTask progressTask = new TimerTask() { |
| | | // Persistent state restore progress report. |
| | | @Override |
| | | public void run() { |
| | | if (processedEntries.get() > 0) { |
| | | long freeMemory = |
| | | Runtime.getRuntime().freeMemory() / bytesPerMegabyte; |
| | | logger.info(NOTE_CACHE_PRELOAD_PROGRESS_REPORT, backend.getBackendID(), processedEntries.get(), freeMemory); |
| | | } |
| | | } |
| | | }; |
| | | timer.scheduleAtFixedRate(progressTask, progressInterval, |
| | | progressInterval); |
| | | // Cycle to monitor progress and adjust workers. |
| | | long processedEntriesCycle = 0; |
| | | long processedEntriesDelta = 0; |
| | | long processedEntriesDeltaLow = 0; |
| | | long processedEntriesDeltaHigh = 0; |
| | | long lastKnownProcessedEntries = 0; |
| | | try { |
| | | while (!entryQueue.isEmpty() || collector.isAlive()) { |
| | | |
| | | Thread.sleep(syncSleepTime); |
| | | |
| | | processedEntriesCycle = processedEntries.get(); |
| | | processedEntriesDelta = |
| | | processedEntriesCycle - lastKnownProcessedEntries; |
| | | lastKnownProcessedEntries = processedEntriesCycle; |
| | | // Spawn another worker if scaling up. |
| | | if (processedEntriesDelta > processedEntriesDeltaHigh) { |
| | | processedEntriesDeltaLow = processedEntriesDeltaHigh; |
| | | processedEntriesDeltaHigh = processedEntriesDelta; |
| | | EntryCachePreloadWorker workerThread = |
| | | new EntryCachePreloadWorker(); |
| | | workerThread.start(); |
| | | preloadThreads.add(workerThread); |
| | | } |
| | | // Interrupt random worker if scaling down. |
| | | if (processedEntriesDelta < processedEntriesDeltaLow) { |
| | | processedEntriesDeltaHigh = processedEntriesDeltaLow; |
| | | processedEntriesDeltaLow = processedEntriesDelta; |
| | | // Leave at least one worker to progress. |
| | | if (preloadThreads.size() > 1) { |
| | | interruptFlag.set(true); |
| | | } |
| | | } |
| | | } |
| | | // Join the collector. |
| | | if (collector.isAlive()) { |
| | | collector.join(); |
| | | } |
| | | // Join all spawned workers. |
| | | for (Thread workerThread : preloadThreads) { |
| | | if (workerThread.isAlive()) { |
| | | workerThread.join(); |
| | | } |
| | | } |
| | | // Cancel progress report task and report done. |
| | | timer.cancel(); |
| | | logger.info(NOTE_CACHE_PRELOAD_PROGRESS_DONE, backend.getBackendID(), processedEntries.get()); |
| | | } catch (InterruptedException ex) { |
| | | logger.traceException(ex); |
| | | // Interrupt the collector. |
| | | collector.interrupt(); |
| | | // Interrupt all preload threads. |
| | | for (Thread thread : preloadThreads) { |
| | | thread.interrupt(); |
| | | } |
| | | logger.warn(WARN_CACHE_PRELOAD_INTERRUPTED, backend.getBackendID()); |
| | | } finally { |
| | | // Kill the timer task. |
| | | timer.cancel(); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * The worker thread. |
| | | */ |
| | | private class EntryCachePreloadWorker extends DirectoryThread { |
| | | public EntryCachePreloadWorker() { |
| | | super("Entry Cache Preload Worker"); |
| | | } |
| | | @Override |
| | | public void run() { |
| | | while (!entryQueue.isEmpty() || collector.isAlive()) { |
| | | // Check if interrupted. |
| | | if (Thread.interrupted()) { |
| | | return; |
| | | } |
| | | // Check for scaling down interruption. |
| | | if (interruptFlag.compareAndSet(true, false)) { |
| | | preloadThreads.remove(Thread.currentThread()); |
| | | break; |
| | | } |
| | | // Dequeue the next entry. |
| | | try { |
| | | PreloadEntry preloadEntry = entryQueue.poll(); |
| | | if (preloadEntry == null) { |
| | | continue; |
| | | } |
| | | long entryID = preloadEntry.entryIDBytes.toLong(); |
| | | Entry entry = |
| | | ID2Entry.entryFromDatabase(preloadEntry.entryBytes, |
| | | backend.getRootContainer().getCompressedSchema()); |
| | | try { |
| | | // Even if the entry does not end up in the cache its still |
| | | // treated as a processed entry anyways. |
| | | DirectoryServer.getEntryCache().putEntry(entry, backend, entryID); |
| | | processedEntries.getAndIncrement(); |
| | | } catch (Exception ex) { |
| | | logger.traceException(ex); |
| | | logger.error(ERR_CACHE_PRELOAD_ENTRY_FAILED, entry.getName(), |
| | | (ex.getCause() != null ? ex.getCause().getMessage() : |
| | | stackTraceToSingleLineString(ex))); |
| | | } |
| | | } catch (Exception ex) { |
| | | break; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * The Collector thread. |
| | | */ |
| | | private class EntryCacheCollector extends DirectoryThread { |
| | | public EntryCacheCollector() { |
| | | super("Entry Cache Preload Collector"); |
| | | } |
| | | @Override |
| | | public void run() { |
| | | Cursor cursor = null; |
| | | ID2Entry id2entry = null; |
| | | Collection<EntryContainer> entryContainers = |
| | | backend.getRootContainer().getEntryContainers(); |
| | | Iterator<EntryContainer> ecIterator = |
| | | entryContainers.iterator(); |
| | | boolean success = true; |
| | | |
| | | try { |
| | | while (success) { |
| | | // Check if interrupted. |
| | | if (Thread.interrupted()) { |
| | | return; |
| | | } |
| | | try { |
| | | if (cursor == null) { |
| | | if (ecIterator.hasNext()) { |
| | | id2entry = ecIterator.next().getID2Entry(); |
| | | } else { |
| | | break; |
| | | } |
| | | if (id2entry != null) { |
| | | cursor = id2entry.openCursor(null); |
| | | } else { |
| | | continue; |
| | | } |
| | | } |
| | | // BUG cursor might be null ? If not why testing below ? |
| | | success = cursor.next(); |
| | | if (!success) { |
| | | // Reset cursor and continue. |
| | | close(cursor); |
| | | success = true; |
| | | cursor = null; |
| | | } else { |
| | | entryQueue.put(new PreloadEntry(cursor.getValue(), cursor.getKey())); |
| | | } |
| | | } catch (InterruptedException e) { |
| | | return; |
| | | } catch (Exception e) { |
| | | logger.traceException(e); |
| | | } |
| | | } |
| | | } finally { |
| | | close(cursor); |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * This inner class represents pre-load entry object. |
| | | */ |
| | | private class PreloadEntry { |
| | | |
| | | // Encoded Entry. |
| | | public ByteString entryBytes; |
| | | |
| | | // Encoded EntryID. |
| | | public ByteString entryIDBytes; |
| | | |
| | | /** |
| | | * Default constructor. |
| | | */ |
| | | public PreloadEntry(ByteString entryBytes, ByteString entryIDBytes) |
| | | { |
| | | this.entryBytes = entryBytes; |
| | | this.entryIDBytes = entryIDBytes; |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | /* |
| | | * CDDL HEADER START |
| | | * |
| | | * The contents of this file are subject to the terms of the |
| | | * Common Development and Distribution License, Version 1.0 only |
| | | * (the "License"). You may not use this file except in compliance |
| | | * with the License. |
| | | * |
| | | * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt |
| | | * or http://forgerock.org/license/CDDLv1.0.html. |
| | | * See the License for the specific language governing permissions |
| | | * and limitations under the License. |
| | | * |
| | | * When distributing Covered Code, include this CDDL HEADER in each |
| | | * file and include the License file at legal-notices/CDDLv1_0.txt. |
| | | * If applicable, add the following below this CDDL HEADER, with the |
| | | * fields enclosed by brackets "[]" replaced with your own identifying |
| | | * information: |
| | | * Portions Copyright [yyyy] [name of copyright owner] |
| | | * |
| | | * CDDL HEADER END |
| | | * |
| | | * |
| | | * Copyright 2006-2010 Sun Microsystems, Inc. |
| | | * Portions Copyright 2011-2014 ForgeRock AS |
| | | * Portions copyright 2013 Manuel Gaupp |
| | | */ |
| | | package org.opends.server.backends.pluggable; |
| | | |
| | | import java.util.*; |
| | | import java.util.concurrent.locks.Lock; |
| | | import java.util.concurrent.locks.ReentrantReadWriteLock; |
| | | |
| | | import org.forgerock.i18n.LocalizableMessage; |
| | | import org.forgerock.i18n.LocalizableMessageBuilder; |
| | | import org.forgerock.i18n.slf4j.LocalizedLogger; |
| | | import org.forgerock.opendj.config.server.ConfigException; |
| | | import org.forgerock.opendj.ldap.ByteSequence; |
| | | import org.forgerock.opendj.ldap.ByteString; |
| | | import org.forgerock.opendj.ldap.ByteStringBuilder; |
| | | import org.forgerock.opendj.ldap.ResultCode; |
| | | import org.forgerock.opendj.ldap.SearchScope; |
| | | import org.forgerock.util.Utils; |
| | | import org.opends.server.admin.server.ConfigurationAddListener; |
| | | import org.opends.server.admin.server.ConfigurationChangeListener; |
| | | import org.opends.server.admin.server.ConfigurationDeleteListener; |
| | | import org.opends.server.admin.std.server.LocalDBBackendCfg; |
| | | import org.opends.server.admin.std.server.LocalDBIndexCfg; |
| | | import org.opends.server.admin.std.server.LocalDBVLVIndexCfg; |
| | | import org.opends.server.api.Backend; |
| | | import org.opends.server.api.ClientConnection; |
| | | import org.opends.server.api.EntryCache; |
| | | import org.opends.server.api.plugin.PluginResult.SubordinateDelete; |
| | | import org.opends.server.api.plugin.PluginResult.SubordinateModifyDN; |
| | | import org.opends.server.backends.pluggable.BackendImpl.Cursor; |
| | | import org.opends.server.backends.pluggable.BackendImpl.ReadOperation; |
| | | import org.opends.server.backends.pluggable.BackendImpl.ReadableStorage; |
| | | import org.opends.server.backends.pluggable.BackendImpl.Storage; |
| | | import org.opends.server.backends.pluggable.BackendImpl.StorageRuntimeException; |
| | | import org.opends.server.backends.pluggable.BackendImpl.TreeName; |
| | | import org.opends.server.backends.pluggable.BackendImpl.WriteOperation; |
| | | import org.opends.server.backends.pluggable.BackendImpl.WriteableStorage; |
| | | import org.opends.server.backends.pluggable.SuffixContainer; |
| | | import org.opends.server.controls.*; |
| | | import org.opends.server.core.*; |
| | | import org.opends.server.types.*; |
| | | import org.opends.server.util.ServerConstants; |
| | | import org.opends.server.util.StaticUtils; |
| | | |
| | | import static org.opends.messages.JebMessages.*; |
| | | import static org.opends.server.backends.pluggable.JebFormat.*; |
| | | import static org.opends.server.core.DirectoryServer.*; |
| | | import static org.opends.server.protocols.ldap.LDAPResultCode.*; |
| | | import static org.opends.server.types.AdditionalLogItem.*; |
| | | import static org.opends.server.util.StaticUtils.*; |
| | | |
| | | /** |
| | | * Storage container for LDAP entries. Each base DN of a JE backend is given |
| | | * its own entry container. The entry container is the object that implements |
| | | * the guts of the backend API methods for LDAP operations. |
| | | */ |
| | | public class EntryContainer |
| | | implements SuffixContainer, ConfigurationChangeListener<LocalDBBackendCfg> |
| | | { |
| | | private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); |
| | | |
| | | /** The name of the entry database. */ |
| | | public static final String ID2ENTRY_DATABASE_NAME = ID2ENTRY_INDEX_NAME; |
| | | /** The name of the DN database. */ |
| | | public static final String DN2ID_DATABASE_NAME = DN2ID_INDEX_NAME; |
| | | /** The name of the children index database. */ |
| | | private static final String ID2CHILDREN_DATABASE_NAME = ID2CHILDREN_INDEX_NAME; |
| | | /** The name of the subtree index database. */ |
| | | private static final String ID2SUBTREE_DATABASE_NAME = ID2SUBTREE_INDEX_NAME; |
| | | /** The name of the referral database. */ |
| | | private static final String REFERRAL_DATABASE_NAME = REFERRAL_INDEX_NAME; |
| | | /** The name of the state database. */ |
| | | private static final String STATE_DATABASE_NAME = STATE_INDEX_NAME; |
| | | |
| | | /** The attribute index configuration manager. */ |
| | | private final AttributeJEIndexCfgManager attributeJEIndexCfgManager; |
| | | /** The vlv index configuration manager. */ |
| | | private final VLVJEIndexCfgManager vlvJEIndexCfgManager; |
| | | |
| | | /** The backend to which this entry container belongs. */ |
| | | private final Backend<?> backend; |
| | | |
| | | /** The root container in which this entryContainer belongs. */ |
| | | private final RootContainer rootContainer; |
| | | |
| | | /** The baseDN this entry container is responsible for. */ |
| | | private final DN baseDN; |
| | | |
| | | /** The backend configuration. */ |
| | | private LocalDBBackendCfg config; |
| | | |
| | | /** The JE database environment. */ |
| | | private final Storage storage; |
| | | |
| | | /** The DN database maps a normalized DN string to an entry ID (8 bytes). */ |
| | | private DN2ID dn2id; |
| | | /** The entry database maps an entry ID (8 bytes) to a complete encoded entry. */ |
| | | private ID2Entry id2entry; |
| | | /** Index maps entry ID to an entry ID list containing its children. */ |
| | | private Index id2children; |
| | | /** Index maps entry ID to an entry ID list containing its subordinates. */ |
| | | private Index id2subtree; |
| | | /** The referral database maps a normalized DN string to labeled URIs. */ |
| | | private DN2URI dn2uri; |
| | | /** The state database maps a config DN to config entries. */ |
| | | private State state; |
| | | |
| | | /** The set of attribute indexes. */ |
| | | private final HashMap<AttributeType, AttributeIndex> attrIndexMap = new HashMap<AttributeType, AttributeIndex>(); |
| | | |
| | | /** The set of VLV (Virtual List View) indexes. */ |
| | | private final HashMap<String, VLVIndex> vlvIndexMap = new HashMap<String, VLVIndex>(); |
| | | |
| | | /** |
| | | * Prevents name clashes for common indexes (like id2entry) across multiple suffixes. |
| | | * For example when a root container contains multiple suffixes. |
| | | */ |
| | | private TreeName databasePrefix; |
| | | |
| | | /** |
| | | * This class is responsible for managing the configuration for attribute |
| | | * indexes used within this entry container. |
| | | */ |
| | | private class AttributeJEIndexCfgManager implements |
| | | ConfigurationAddListener<LocalDBIndexCfg>, |
| | | ConfigurationDeleteListener<LocalDBIndexCfg> |
| | | { |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public boolean isConfigurationAddAcceptable( |
| | | LocalDBIndexCfg cfg, |
| | | List<LocalizableMessage> unacceptableReasons) |
| | | { |
| | | try |
| | | { |
| | | //Try creating all the indexes before confirming they are valid ones. |
| | | new AttributeIndex(cfg, EntryContainer.this); |
| | | return true; |
| | | } |
| | | catch(Exception e) |
| | | { |
| | | unacceptableReasons.add(LocalizableMessage.raw(e.getLocalizedMessage())); |
| | | return false; |
| | | } |
| | | } |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public ConfigChangeResult applyConfigurationAdd(LocalDBIndexCfg cfg) |
| | | { |
| | | boolean adminActionRequired = false; |
| | | List<LocalizableMessage> messages = new ArrayList<LocalizableMessage>(); |
| | | |
| | | try |
| | | { |
| | | AttributeIndex index = new AttributeIndex(cfg, EntryContainer.this); |
| | | index.open(); |
| | | if(!index.isTrusted()) |
| | | { |
| | | adminActionRequired = true; |
| | | messages.add(NOTE_JEB_INDEX_ADD_REQUIRES_REBUILD.get( |
| | | cfg.getAttribute().getNameOrOID())); |
| | | } |
| | | attrIndexMap.put(cfg.getAttribute(), index); |
| | | } |
| | | catch(Exception e) |
| | | { |
| | | messages.add(LocalizableMessage.raw(e.getLocalizedMessage())); |
| | | return new ConfigChangeResult( |
| | | DirectoryServer.getServerErrorResultCode(), adminActionRequired, messages); |
| | | } |
| | | |
| | | return new ConfigChangeResult(ResultCode.SUCCESS, adminActionRequired, messages); |
| | | } |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public boolean isConfigurationDeleteAcceptable( |
| | | LocalDBIndexCfg cfg, List<LocalizableMessage> unacceptableReasons) |
| | | { |
| | | // TODO: validate more before returning true? |
| | | return true; |
| | | } |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public ConfigChangeResult applyConfigurationDelete(LocalDBIndexCfg cfg) |
| | | { |
| | | boolean adminActionRequired = false; |
| | | ArrayList<LocalizableMessage> messages = new ArrayList<LocalizableMessage>(); |
| | | |
| | | exclusiveLock.lock(); |
| | | try |
| | | { |
| | | AttributeIndex index = attrIndexMap.get(cfg.getAttribute()); |
| | | deleteAttributeIndex(index); |
| | | attrIndexMap.remove(cfg.getAttribute()); |
| | | } |
| | | catch(StorageRuntimeException de) |
| | | { |
| | | messages.add(LocalizableMessage.raw(StaticUtils.stackTraceToSingleLineString(de))); |
| | | return new ConfigChangeResult( |
| | | DirectoryServer.getServerErrorResultCode(), adminActionRequired, messages); |
| | | } |
| | | finally |
| | | { |
| | | exclusiveLock.unlock(); |
| | | } |
| | | |
| | | return new ConfigChangeResult(ResultCode.SUCCESS, adminActionRequired, messages); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * This class is responsible for managing the configuration for VLV indexes |
| | | * used within this entry container. |
| | | */ |
| | | private class VLVJEIndexCfgManager implements |
| | | ConfigurationAddListener<LocalDBVLVIndexCfg>, |
| | | ConfigurationDeleteListener<LocalDBVLVIndexCfg> |
| | | { |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public boolean isConfigurationAddAcceptable( |
| | | LocalDBVLVIndexCfg cfg, List<LocalizableMessage> unacceptableReasons) |
| | | { |
| | | try |
| | | { |
| | | SearchFilter.createFilterFromString(cfg.getFilter()); |
| | | } |
| | | catch(Exception e) |
| | | { |
| | | LocalizableMessage msg = ERR_JEB_CONFIG_VLV_INDEX_BAD_FILTER.get( |
| | | cfg.getFilter(), cfg.getName(), |
| | | e.getLocalizedMessage()); |
| | | unacceptableReasons.add(msg); |
| | | return false; |
| | | } |
| | | |
| | | String[] sortAttrs = cfg.getSortOrder().split(" "); |
| | | SortKey[] sortKeys = new SortKey[sortAttrs.length]; |
| | | boolean[] ascending = new boolean[sortAttrs.length]; |
| | | for(int i = 0; i < sortAttrs.length; i++) |
| | | { |
| | | try |
| | | { |
| | | if(sortAttrs[i].startsWith("-")) |
| | | { |
| | | ascending[i] = false; |
| | | sortAttrs[i] = sortAttrs[i].substring(1); |
| | | } |
| | | else |
| | | { |
| | | ascending[i] = true; |
| | | if(sortAttrs[i].startsWith("+")) |
| | | { |
| | | sortAttrs[i] = sortAttrs[i].substring(1); |
| | | } |
| | | } |
| | | } |
| | | catch(Exception e) |
| | | { |
| | | LocalizableMessage msg = |
| | | ERR_JEB_CONFIG_VLV_INDEX_UNDEFINED_ATTR.get(sortKeys[i], cfg.getName()); |
| | | unacceptableReasons.add(msg); |
| | | return false; |
| | | } |
| | | |
| | | AttributeType attrType = |
| | | DirectoryServer.getAttributeType(sortAttrs[i].toLowerCase()); |
| | | if(attrType == null) |
| | | { |
| | | LocalizableMessage msg = ERR_JEB_CONFIG_VLV_INDEX_UNDEFINED_ATTR.get( |
| | | sortAttrs[i], cfg.getName()); |
| | | unacceptableReasons.add(msg); |
| | | return false; |
| | | } |
| | | sortKeys[i] = new SortKey(attrType, ascending[i]); |
| | | } |
| | | |
| | | return true; |
| | | } |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public ConfigChangeResult applyConfigurationAdd(LocalDBVLVIndexCfg cfg) |
| | | { |
| | | boolean adminActionRequired = false; |
| | | ArrayList<LocalizableMessage> messages = new ArrayList<LocalizableMessage>(); |
| | | |
| | | try |
| | | { |
| | | VLVIndex vlvIndex = new VLVIndex(cfg, state, storage, EntryContainer.this); |
| | | vlvIndex.open(); |
| | | if(!vlvIndex.isTrusted()) |
| | | { |
| | | adminActionRequired = true; |
| | | messages.add(NOTE_JEB_INDEX_ADD_REQUIRES_REBUILD.get( |
| | | cfg.getName())); |
| | | } |
| | | vlvIndexMap.put(cfg.getName().toLowerCase(), vlvIndex); |
| | | } |
| | | catch(Exception e) |
| | | { |
| | | messages.add(LocalizableMessage.raw(StaticUtils.stackTraceToSingleLineString(e))); |
| | | return new ConfigChangeResult( |
| | | DirectoryServer.getServerErrorResultCode(), adminActionRequired, messages); |
| | | } |
| | | |
| | | return new ConfigChangeResult(ResultCode.SUCCESS, adminActionRequired, messages); |
| | | } |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public boolean isConfigurationDeleteAcceptable( |
| | | LocalDBVLVIndexCfg cfg, |
| | | List<LocalizableMessage> unacceptableReasons) |
| | | { |
| | | // TODO: validate more before returning true? |
| | | return true; |
| | | } |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public ConfigChangeResult applyConfigurationDelete(LocalDBVLVIndexCfg cfg) |
| | | { |
| | | boolean adminActionRequired = false; |
| | | List<LocalizableMessage> messages = new ArrayList<LocalizableMessage>(); |
| | | |
| | | exclusiveLock.lock(); |
| | | try |
| | | { |
| | | VLVIndex vlvIndex = |
| | | vlvIndexMap.get(cfg.getName().toLowerCase()); |
| | | deleteDatabase(vlvIndex); |
| | | vlvIndexMap.remove(cfg.getName()); |
| | | } |
| | | catch(StorageRuntimeException de) |
| | | { |
| | | messages.add(LocalizableMessage.raw(StaticUtils.stackTraceToSingleLineString(de))); |
| | | return new ConfigChangeResult( |
| | | DirectoryServer.getServerErrorResultCode(), adminActionRequired, messages); |
| | | } |
| | | finally |
| | | { |
| | | exclusiveLock.unlock(); |
| | | } |
| | | |
| | | return new ConfigChangeResult(ResultCode.SUCCESS, adminActionRequired, messages); |
| | | } |
| | | |
| | | } |
| | | |
| | | /** A read write lock to handle schema changes and bulk changes. */ |
| | | private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); |
| | | final Lock sharedLock = lock.readLock(); |
| | | final Lock exclusiveLock = lock.writeLock(); |
| | | |
| | | /** |
| | | * Create a new entry container object. |
| | | * |
| | | * @param baseDN The baseDN this entry container will be responsible for |
| | | * storing on disk. |
| | | * @param databasePrefix The prefix to use in the database names used by |
| | | * this entry container. |
| | | * @param backend A reference to the JE backend that is creating this entry |
| | | * container. It is needed by the Directory Server entry cache |
| | | * methods. |
| | | * @param config The configuration of the JE backend. |
| | | * @param env The JE environment to create this entryContainer in. |
| | | * @param rootContainer The root container this entry container is in. |
| | | * @throws ConfigException if a configuration related error occurs. |
| | | */ |
| | | public EntryContainer(DN baseDN, String databasePrefix, Backend<?> backend, |
| | | LocalDBBackendCfg config, Storage env, RootContainer rootContainer) |
| | | throws ConfigException |
| | | { |
| | | this.backend = backend; |
| | | this.baseDN = baseDN; |
| | | this.config = config; |
| | | this.storage = env; |
| | | this.rootContainer = rootContainer; |
| | | |
| | | this.databasePrefix = preparePrefix(databasePrefix); |
| | | |
| | | config.addLocalDBChangeListener(this); |
| | | |
| | | attributeJEIndexCfgManager = new AttributeJEIndexCfgManager(); |
| | | config.addLocalDBIndexAddListener(attributeJEIndexCfgManager); |
| | | config.addLocalDBIndexDeleteListener(attributeJEIndexCfgManager); |
| | | |
| | | vlvJEIndexCfgManager = new VLVJEIndexCfgManager(); |
| | | config.addLocalDBVLVIndexAddListener(vlvJEIndexCfgManager); |
| | | config.addLocalDBVLVIndexDeleteListener(vlvJEIndexCfgManager); |
| | | } |
| | | |
| | | /** |
| | | * Opens the entryContainer for reading and writing. |
| | | * |
| | | * @throws StorageRuntimeException If an error occurs in the JE database. |
| | | * @throws ConfigException if a configuration related error occurs. |
| | | */ |
| | | public void open() |
| | | throws StorageRuntimeException, ConfigException |
| | | { |
| | | try |
| | | { |
| | | DataConfig entryDataConfig = |
| | | new DataConfig(config.isEntriesCompressed(), |
| | | config.isCompactEncoding(), |
| | | rootContainer.getCompressedSchema()); |
| | | |
| | | id2entry = new ID2Entry(databasePrefix.child(ID2ENTRY_DATABASE_NAME), |
| | | entryDataConfig, storage, this); |
| | | id2entry.open(); |
| | | |
| | | dn2id = new DN2ID(databasePrefix.child(DN2ID_DATABASE_NAME), storage, this); |
| | | dn2id.open(); |
| | | |
| | | state = new State(databasePrefix.child(STATE_DATABASE_NAME), storage, this); |
| | | state.open(); |
| | | |
| | | if (config.isSubordinateIndexesEnabled()) |
| | | { |
| | | openSubordinateIndexes(); |
| | | } |
| | | else |
| | | { |
| | | // Use a null index and ensure that future attempts to use the real |
| | | // subordinate indexes will fail. |
| | | id2children = new NullIndex(databasePrefix.child(ID2CHILDREN_DATABASE_NAME), |
| | | new ID2CIndexer(), state, storage, this); |
| | | if (!storage.getConfig().getReadOnly()) |
| | | { |
| | | state.putIndexTrustState(null, id2children, false); |
| | | } |
| | | id2children.open(); // No-op |
| | | |
| | | id2subtree = new NullIndex(databasePrefix.child(ID2SUBTREE_DATABASE_NAME), |
| | | new ID2SIndexer(), state, storage, this); |
| | | if (!storage.getConfig().getReadOnly()) |
| | | { |
| | | state.putIndexTrustState(null, id2subtree, false); |
| | | } |
| | | id2subtree.open(); // No-op |
| | | |
| | | logger.info(NOTE_JEB_SUBORDINATE_INDEXES_DISABLED, backend.getBackendID()); |
| | | } |
| | | |
| | | dn2uri = new DN2URI(databasePrefix.child(REFERRAL_DATABASE_NAME), storage, this); |
| | | dn2uri.open(); |
| | | |
| | | for (String idx : config.listLocalDBIndexes()) |
| | | { |
| | | LocalDBIndexCfg indexCfg = config.getLocalDBIndex(idx); |
| | | |
| | | AttributeIndex index = new AttributeIndex(indexCfg, this); |
| | | index.open(); |
| | | if(!index.isTrusted()) |
| | | { |
| | | logger.info(NOTE_JEB_INDEX_ADD_REQUIRES_REBUILD, index.getName()); |
| | | } |
| | | attrIndexMap.put(indexCfg.getAttribute(), index); |
| | | } |
| | | |
| | | for(String idx : config.listLocalDBVLVIndexes()) |
| | | { |
| | | LocalDBVLVIndexCfg vlvIndexCfg = config.getLocalDBVLVIndex(idx); |
| | | |
| | | VLVIndex vlvIndex = new VLVIndex(vlvIndexCfg, state, storage, this); |
| | | vlvIndex.open(); |
| | | |
| | | if(!vlvIndex.isTrusted()) |
| | | { |
| | | logger.info(NOTE_JEB_INDEX_ADD_REQUIRES_REBUILD, vlvIndex.getName()); |
| | | } |
| | | |
| | | vlvIndexMap.put(vlvIndexCfg.getName().toLowerCase(), vlvIndex); |
| | | } |
| | | } |
| | | catch (StorageRuntimeException de) |
| | | { |
| | | logger.traceException(de); |
| | | close(); |
| | | throw de; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Closes the entry container. |
| | | * |
| | | * @throws StorageRuntimeException If an error occurs in the JE database. |
| | | */ |
| | | @Override |
| | | public void close() throws StorageRuntimeException |
| | | { |
| | | // Close core indexes. |
| | | dn2id.close(); |
| | | id2entry.close(); |
| | | dn2uri.close(); |
| | | id2children.close(); |
| | | id2subtree.close(); |
| | | state.close(); |
| | | |
| | | Utils.closeSilently(attrIndexMap.values()); |
| | | |
| | | for (VLVIndex vlvIndex : vlvIndexMap.values()) |
| | | { |
| | | vlvIndex.close(); |
| | | } |
| | | |
| | | // Deregister any listeners. |
| | | config.removeLocalDBChangeListener(this); |
| | | config.removeLocalDBIndexAddListener(attributeJEIndexCfgManager); |
| | | config.removeLocalDBIndexDeleteListener(attributeJEIndexCfgManager); |
| | | config.removeLocalDBVLVIndexAddListener(vlvJEIndexCfgManager); |
| | | config.removeLocalDBVLVIndexDeleteListener(vlvJEIndexCfgManager); |
| | | } |
| | | |
| | | /** |
| | | * Retrieves a reference to the root container in which this entry container |
| | | * exists. |
| | | * |
| | | * @return A reference to the root container in which this entry container |
| | | * exists. |
| | | */ |
| | | public RootContainer getRootContainer() |
| | | { |
| | | return rootContainer; |
| | | } |
| | | |
| | | public Storage getStorage() |
| | | { |
| | | return storage; |
| | | } |
| | | |
| | | /** |
| | | * Get the DN database used by this entry container. |
| | | * The entryContainer must have been opened. |
| | | * |
| | | * @return The DN database. |
| | | */ |
| | | public DN2ID getDN2ID() |
| | | { |
| | | return dn2id; |
| | | } |
| | | |
| | | /** |
| | | * Get the entry database used by this entry container. |
| | | * The entryContainer must have been opened. |
| | | * |
| | | * @return The entry database. |
| | | */ |
| | | public ID2Entry getID2Entry() |
| | | { |
| | | return id2entry; |
| | | } |
| | | |
| | | /** |
| | | * Get the referral database used by this entry container. |
| | | * The entryContainer must have been opened. |
| | | * |
| | | * @return The referral database. |
| | | */ |
| | | public DN2URI getDN2URI() |
| | | { |
| | | return dn2uri; |
| | | } |
| | | |
| | | /** |
| | | * Get the children database used by this entry container. |
| | | * The entryContainer must have been opened. |
| | | * |
| | | * @return The children database. |
| | | */ |
| | | public Index getID2Children() |
| | | { |
| | | return id2children; |
| | | } |
| | | |
| | | /** |
| | | * Get the subtree database used by this entry container. |
| | | * The entryContainer must have been opened. |
| | | * |
| | | * @return The subtree database. |
| | | */ |
| | | public Index getID2Subtree() |
| | | { |
| | | return id2subtree; |
| | | } |
| | | |
| | | /** |
| | | * Get the state database used by this entry container. |
| | | * The entry container must have been opened. |
| | | * |
| | | * @return The state database. |
| | | */ |
| | | public State getState() |
| | | { |
| | | return state; |
| | | } |
| | | |
| | | /** |
| | | * Look for an attribute index for the given attribute type. |
| | | * |
| | | * @param attrType The attribute type for which an attribute index is needed. |
| | | * @return The attribute index or null if there is none for that type. |
| | | */ |
| | | public AttributeIndex getAttributeIndex(AttributeType attrType) |
| | | { |
| | | return attrIndexMap.get(attrType); |
| | | } |
| | | |
| | | /** |
| | | * Return attribute index map. |
| | | * |
| | | * @return The attribute index map. |
| | | */ |
| | | public Map<AttributeType, AttributeIndex> getAttributeIndexMap() { |
| | | return attrIndexMap; |
| | | } |
| | | |
| | | /** |
| | | * Look for an VLV index for the given index name. |
| | | * |
| | | * @param vlvIndexName The vlv index name for which an vlv index is needed. |
| | | * @return The VLV index or null if there is none with that name. |
| | | */ |
| | | public VLVIndex getVLVIndex(String vlvIndexName) |
| | | { |
| | | return vlvIndexMap.get(vlvIndexName); |
| | | } |
| | | |
| | | /** |
| | | * Retrieve all attribute indexes. |
| | | * |
| | | * @return All attribute indexes defined in this entry container. |
| | | */ |
| | | public Collection<AttributeIndex> getAttributeIndexes() |
| | | { |
| | | return attrIndexMap.values(); |
| | | } |
| | | |
| | | /** |
| | | * Retrieve all VLV indexes. |
| | | * |
| | | * @return The collection of VLV indexes defined in this entry container. |
| | | */ |
| | | public Collection<VLVIndex> getVLVIndexes() |
| | | { |
| | | return vlvIndexMap.values(); |
| | | } |
| | | |
| | | /** |
| | | * Determine the highest entryID in the entryContainer. |
| | | * The entryContainer must already be open. |
| | | * |
| | | * @return The highest entry ID. |
| | | * @throws StorageRuntimeException If an error occurs in the JE database. |
| | | */ |
| | | public EntryID getHighestEntryID() throws StorageRuntimeException |
| | | { |
| | | Cursor cursor = storage.openCursor(id2entry.getName()); |
| | | try |
| | | { |
| | | // Position a cursor on the last data item, and the key should give the highest ID. |
| | | if (cursor.positionToLastKey()) |
| | | { |
| | | return new EntryID(cursor.getKey()); |
| | | } |
| | | return new EntryID(0); |
| | | } |
| | | finally |
| | | { |
| | | cursor.close(); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Determine the number of subordinate entries for a given entry. |
| | | * |
| | | * @param entryDN The distinguished name of the entry. |
| | | * @param subtree <code>true</code> will include all the entries under the |
| | | * given entries. <code>false</code> will only return the |
| | | * number of entries immediately under the given entry. |
| | | * @return The number of subordinate entries for the given entry or -1 if |
| | | * the entry does not exist. |
| | | * @throws StorageRuntimeException If an error occurs in the JE database. |
| | | */ |
| | | public long getNumSubordinates(final DN entryDN, final boolean subtree) |
| | | throws StorageRuntimeException |
| | | { |
| | | try |
| | | { |
| | | return storage.read(new ReadOperation<Long>() |
| | | { |
| | | @Override |
| | | public Long run(ReadableStorage txn) throws Exception |
| | | { |
| | | EntryID entryID = dn2id.get(txn, entryDN, false); |
| | | if (entryID != null) |
| | | { |
| | | ByteString key = entryID.toByteString(); |
| | | EntryIDSet entryIDSet; |
| | | if (subtree) |
| | | { |
| | | entryIDSet = id2subtree.readKey(key, txn); |
| | | } |
| | | else |
| | | { |
| | | entryIDSet = id2children.readKey(key, txn); |
| | | } |
| | | long count = entryIDSet.size(); |
| | | if (count != Long.MAX_VALUE) |
| | | { |
| | | return count; |
| | | } |
| | | } |
| | | return -1L; |
| | | } |
| | | }); |
| | | } |
| | | catch (Exception e) |
| | | { |
| | | throw new StorageRuntimeException(e); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Processes the specified search in this entryContainer. |
| | | * Matching entries should be provided back to the core server using the |
| | | * <CODE>SearchOperation.returnEntry</CODE> method. |
| | | * |
| | | * @param searchOperation The search operation to be processed. |
| | | * @throws DirectoryException |
| | | * If a problem occurs while processing the |
| | | * search. |
| | | * @throws StorageRuntimeException If an error occurs in the JE database. |
| | | * @throws CanceledOperationException if this operation should be cancelled. |
| | | */ |
| | | public void search(final SearchOperation searchOperation) |
| | | throws DirectoryException, StorageRuntimeException, CanceledOperationException |
| | | { |
| | | try |
| | | { |
| | | storage.read(new ReadOperation<Void>() |
| | | { |
| | | @Override |
| | | public Void run(ReadableStorage txn) throws Exception |
| | | { |
| | | DN aBaseDN = searchOperation.getBaseDN(); |
| | | SearchScope searchScope = searchOperation.getScope(); |
| | | |
| | | PagedResultsControl pageRequest = searchOperation.getRequestControl(PagedResultsControl.DECODER); |
| | | ServerSideSortRequestControl sortRequest = |
| | | searchOperation.getRequestControl(ServerSideSortRequestControl.DECODER); |
| | | if (sortRequest != null && !sortRequest.containsSortKeys() && sortRequest.isCritical()) |
| | | { |
| | | /** |
| | | * If the control's criticality field is true then the server SHOULD |
| | | * do the following: return unavailableCriticalExtension as a return |
| | | * code in the searchResultDone message; include the |
| | | * sortKeyResponseControl in the searchResultDone message, and not |
| | | * send back any search result entries. |
| | | */ |
| | | searchOperation.addResponseControl(new ServerSideSortResponseControl(NO_SUCH_ATTRIBUTE, null)); |
| | | searchOperation.setResultCode(ResultCode.UNAVAILABLE_CRITICAL_EXTENSION); |
| | | return null; |
| | | } |
| | | VLVRequestControl vlvRequest = searchOperation.getRequestControl(VLVRequestControl.DECODER); |
| | | |
| | | if (vlvRequest != null && pageRequest != null) |
| | | { |
| | | LocalizableMessage message = ERR_JEB_SEARCH_CANNOT_MIX_PAGEDRESULTS_AND_VLV.get(); |
| | | throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message); |
| | | } |
| | | |
| | | // Handle client abandon of paged results. |
| | | if (pageRequest != null) |
| | | { |
| | | if (pageRequest.getSize() == 0) |
| | | { |
| | | Control control = new PagedResultsControl(pageRequest.isCritical(), 0, null); |
| | | searchOperation.getResponseControls().add(control); |
| | | return null; |
| | | } |
| | | if (searchOperation.getSizeLimit() > 0 && pageRequest.getSize() >= searchOperation.getSizeLimit()) |
| | | { |
| | | // The RFC says : "If the page size is greater than or equal to the |
| | | // sizeLimit value, the server should ignore the control as the |
| | | // request can be satisfied in a single page" |
| | | pageRequest = null; |
| | | } |
| | | } |
| | | |
| | | // Handle base-object search first. |
| | | if (searchScope == SearchScope.BASE_OBJECT) |
| | | { |
| | | // Fetch the base entry. |
| | | Entry baseEntry = fetchBaseEntry(aBaseDN, searchScope); |
| | | |
| | | if (!isManageDsaITOperation(searchOperation)) |
| | | { |
| | | dn2uri.checkTargetForReferral(baseEntry, searchOperation.getScope()); |
| | | } |
| | | |
| | | if (searchOperation.getFilter().matchesEntry(baseEntry)) |
| | | { |
| | | searchOperation.returnEntry(baseEntry, null); |
| | | } |
| | | |
| | | if (pageRequest != null) |
| | | { |
| | | // Indicate no more pages. |
| | | Control control = new PagedResultsControl(pageRequest.isCritical(), 0, null); |
| | | searchOperation.getResponseControls().add(control); |
| | | } |
| | | |
| | | return null; |
| | | } |
| | | |
| | | // Check whether the client requested debug information about the |
| | | // contribution of the indexes to the search. |
| | | StringBuilder debugBuffer = null; |
| | | if (searchOperation.getAttributes().contains(ATTR_DEBUG_SEARCH_INDEX)) |
| | | { |
| | | debugBuffer = new StringBuilder(); |
| | | } |
| | | |
| | | EntryIDSet entryIDList = null; |
| | | boolean candidatesAreInScope = false; |
| | | if (sortRequest != null) |
| | | { |
| | | for (VLVIndex vlvIndex : vlvIndexMap.values()) |
| | | { |
| | | try |
| | | { |
| | | entryIDList = vlvIndex.evaluate(null, searchOperation, sortRequest, vlvRequest, debugBuffer); |
| | | if (entryIDList != null) |
| | | { |
| | | searchOperation.addResponseControl(new ServerSideSortResponseControl(SUCCESS, null)); |
| | | candidatesAreInScope = true; |
| | | break; |
| | | } |
| | | } |
| | | catch (DirectoryException de) |
| | | { |
| | | searchOperation.addResponseControl(new ServerSideSortResponseControl(de.getResultCode().intValue(), |
| | | null)); |
| | | |
| | | if (sortRequest.isCritical()) |
| | | { |
| | | throw de; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | if (entryIDList == null) |
| | | { |
| | | // See if we could use a virtual attribute rule to process the |
| | | // search. |
| | | for (VirtualAttributeRule rule : DirectoryServer.getVirtualAttributes()) |
| | | { |
| | | if (rule.getProvider().isSearchable(rule, searchOperation, true)) |
| | | { |
| | | rule.getProvider().processSearch(rule, searchOperation); |
| | | return null; |
| | | } |
| | | } |
| | | |
| | | // Create an index filter to get the search result candidate entries |
| | | IndexFilter indexFilter = |
| | | new IndexFilter(EntryContainer.this, searchOperation, debugBuffer, rootContainer.getMonitorProvider()); |
| | | |
| | | // Evaluate the filter against the attribute indexes. |
| | | entryIDList = indexFilter.evaluate(); |
| | | |
| | | // Evaluate the search scope against the id2children and id2subtree indexes |
| | | if (entryIDList.size() > IndexFilter.FILTER_CANDIDATE_THRESHOLD) |
| | | { |
| | | // Read the ID from dn2id. |
| | | EntryID baseID = dn2id.get(txn, aBaseDN, false); |
| | | if (baseID == null) |
| | | { |
| | | LocalizableMessage message = ERR_JEB_SEARCH_NO_SUCH_OBJECT.get(aBaseDN); |
| | | DN matchedDN = getMatchedDN(aBaseDN); |
| | | throw new DirectoryException(ResultCode.NO_SUCH_OBJECT, message, matchedDN, null); |
| | | } |
| | | ByteString baseIDData = baseID.toByteString(); |
| | | |
| | | EntryIDSet scopeList; |
| | | if (searchScope == SearchScope.SINGLE_LEVEL) |
| | | { |
| | | scopeList = id2children.readKey(baseIDData, txn); |
| | | } |
| | | else |
| | | { |
| | | scopeList = id2subtree.readKey(baseIDData, txn); |
| | | if (searchScope == SearchScope.WHOLE_SUBTREE) |
| | | { |
| | | // The id2subtree list does not include the base entry ID. |
| | | scopeList.add(baseID); |
| | | } |
| | | } |
| | | entryIDList.retainAll(scopeList); |
| | | if (debugBuffer != null) |
| | | { |
| | | debugBuffer.append(" scope="); |
| | | debugBuffer.append(searchScope); |
| | | scopeList.toString(debugBuffer); |
| | | } |
| | | if (scopeList.isDefined()) |
| | | { |
| | | // In this case we know that every candidate is in scope. |
| | | candidatesAreInScope = true; |
| | | } |
| | | } |
| | | |
| | | if (sortRequest != null) |
| | | { |
| | | try |
| | | { |
| | | // If the sort key is not present, the sorting will generate the |
| | | // default ordering. VLV search request goes through as if |
| | | // this sort key was not found in the user entry. |
| | | entryIDList = |
| | | EntryIDSetSorter.sort(EntryContainer.this, entryIDList, searchOperation, |
| | | sortRequest.getSortOrder(), vlvRequest); |
| | | if (sortRequest.containsSortKeys()) |
| | | { |
| | | searchOperation.addResponseControl(new ServerSideSortResponseControl(SUCCESS, null)); |
| | | } |
| | | else |
| | | { |
| | | /* |
| | | * There is no sort key associated with the sort control. |
| | | * Since it came here it means that the criticality is false |
| | | * so let the server return all search results unsorted and |
| | | * include the sortKeyResponseControl in the searchResultDone |
| | | * message. |
| | | */ |
| | | searchOperation.addResponseControl(new ServerSideSortResponseControl(NO_SUCH_ATTRIBUTE, null)); |
| | | } |
| | | } |
| | | catch (DirectoryException de) |
| | | { |
| | | searchOperation.addResponseControl(new ServerSideSortResponseControl(de.getResultCode().intValue(), |
| | | null)); |
| | | |
| | | if (sortRequest.isCritical()) |
| | | { |
| | | throw de; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | // If requested, construct and return a fictitious entry containing |
| | | // debug information, and no other entries. |
| | | if (debugBuffer != null) |
| | | { |
| | | debugBuffer.append(" final="); |
| | | entryIDList.toString(debugBuffer); |
| | | |
| | | Attribute attr = Attributes.create(ATTR_DEBUG_SEARCH_INDEX, debugBuffer.toString()); |
| | | Entry debugEntry = new Entry(DN.valueOf("cn=debugsearch"), null, null, null); |
| | | debugEntry.addAttribute(attr, new ArrayList<ByteString>()); |
| | | |
| | | searchOperation.returnEntry(debugEntry, null); |
| | | return null; |
| | | } |
| | | |
| | | if (entryIDList.isDefined()) |
| | | { |
| | | if (rootContainer.getMonitorProvider().isFilterUseEnabled()) |
| | | { |
| | | rootContainer.getMonitorProvider().updateIndexedSearchCount(); |
| | | } |
| | | searchIndexed(entryIDList, candidatesAreInScope, searchOperation, pageRequest); |
| | | } |
| | | else |
| | | { |
| | | if (rootContainer.getMonitorProvider().isFilterUseEnabled()) |
| | | { |
| | | rootContainer.getMonitorProvider().updateUnindexedSearchCount(); |
| | | } |
| | | |
| | | searchOperation.addAdditionalLogItem(keyOnly(getClass(), "unindexed")); |
| | | |
| | | // See if we could use a virtual attribute rule to process the |
| | | // search. |
| | | for (VirtualAttributeRule rule : DirectoryServer.getVirtualAttributes()) |
| | | { |
| | | if (rule.getProvider().isSearchable(rule, searchOperation, false)) |
| | | { |
| | | rule.getProvider().processSearch(rule, searchOperation); |
| | | return null; |
| | | } |
| | | } |
| | | |
| | | ClientConnection clientConnection = searchOperation.getClientConnection(); |
| | | if (!clientConnection.hasPrivilege(Privilege.UNINDEXED_SEARCH, searchOperation)) |
| | | { |
| | | LocalizableMessage message = ERR_JEB_SEARCH_UNINDEXED_INSUFFICIENT_PRIVILEGES.get(); |
| | | throw new DirectoryException(ResultCode.INSUFFICIENT_ACCESS_RIGHTS, message); |
| | | } |
| | | |
| | | if (sortRequest != null) |
| | | { |
| | | // FIXME -- Add support for sorting unindexed searches using |
| | | // indexes |
| | | // like DSEE currently does. |
| | | searchOperation.addResponseControl(new ServerSideSortResponseControl(UNWILLING_TO_PERFORM, null)); |
| | | |
| | | if (sortRequest.isCritical()) |
| | | { |
| | | LocalizableMessage message = ERR_JEB_SEARCH_CANNOT_SORT_UNINDEXED.get(); |
| | | throw new DirectoryException(ResultCode.UNAVAILABLE_CRITICAL_EXTENSION, message); |
| | | } |
| | | } |
| | | |
| | | searchNotIndexed(searchOperation, pageRequest); |
| | | } |
| | | return null; |
| | | } |
| | | }); |
| | | } |
| | | catch (Exception e) |
| | | { |
| | | throw new StorageRuntimeException(e); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * We were not able to obtain a set of candidate entry IDs for the |
| | | * search from the indexes. |
| | | * <p> |
| | | * Here we are relying on the DN key order to ensure children are |
| | | * returned after their parents. |
| | | * <ul> |
| | | * <li>iterate through a subtree range of the DN database |
| | | * <li>discard non-children DNs if the search scope is single level |
| | | * <li>fetch the entry by ID from the entry cache or the entry database |
| | | * <li>return the entry if it matches the filter |
| | | * </ul> |
| | | * |
| | | * @param searchOperation The search operation. |
| | | * @param pageRequest A Paged Results control, or null if none. |
| | | * @throws DirectoryException If an error prevented the search from being |
| | | * processed. |
| | | */ |
| | | private void searchNotIndexed(SearchOperation searchOperation, PagedResultsControl pageRequest) |
| | | throws DirectoryException, CanceledOperationException |
| | | { |
| | | DN aBaseDN = searchOperation.getBaseDN(); |
| | | SearchScope searchScope = searchOperation.getScope(); |
| | | boolean manageDsaIT = isManageDsaITOperation(searchOperation); |
| | | |
| | | // The base entry must already have been processed if this is |
| | | // a request for the next page in paged results. So we skip |
| | | // the base entry processing if the cookie is set. |
| | | if (pageRequest == null || pageRequest.getCookie().length() == 0) |
| | | { |
| | | // Fetch the base entry. |
| | | Entry baseEntry = fetchBaseEntry(aBaseDN, searchScope); |
| | | |
| | | if (!manageDsaIT) |
| | | { |
| | | dn2uri.checkTargetForReferral(baseEntry, searchScope); |
| | | } |
| | | |
| | | /* |
| | | * The base entry is only included for whole subtree search. |
| | | */ |
| | | if (searchScope == SearchScope.WHOLE_SUBTREE |
| | | && searchOperation.getFilter().matchesEntry(baseEntry)) |
| | | { |
| | | searchOperation.returnEntry(baseEntry, null); |
| | | } |
| | | |
| | | if (!manageDsaIT |
| | | && !dn2uri.returnSearchReferences(searchOperation) |
| | | && pageRequest != null) |
| | | { |
| | | // Indicate no more pages. |
| | | Control control = new PagedResultsControl(pageRequest.isCritical(), 0, null); |
| | | searchOperation.getResponseControls().add(control); |
| | | } |
| | | } |
| | | |
| | | /* |
| | | * We will iterate forwards through a range of the dn2id keys to |
| | | * find subordinates of the target entry from the top of the tree |
| | | * downwards. For example, any subordinates of "dc=example,dc=com" appear |
| | | * in dn2id with a key ending in ",dc=example,dc=com". The entry |
| | | * "cn=joe,ou=people,dc=example,dc=com" will appear after the entry |
| | | * "ou=people,dc=example,dc=com". |
| | | */ |
| | | ByteString baseDNKey = dnToDNKey(aBaseDN, this.baseDN.size()); |
| | | ByteStringBuilder suffix = copyOf(baseDNKey); |
| | | ByteStringBuilder end = copyOf(baseDNKey); |
| | | |
| | | /* |
| | | * Set the ending value to a value of equal length but slightly |
| | | * greater than the suffix. Since keys are compared in |
| | | * reverse order we must set the first byte (the comma). |
| | | * No possibility of overflow here. |
| | | */ |
| | | suffix.append((byte) 0x00); |
| | | end.append((byte) 0x01); |
| | | |
| | | // Set the starting value. |
| | | ByteSequence begin; |
| | | if (pageRequest != null && pageRequest.getCookie().length() != 0) |
| | | { |
| | | // The cookie contains the DN of the next entry to be returned. |
| | | try |
| | | { |
| | | begin = ByteString.wrap(pageRequest.getCookie().toByteArray()); |
| | | } |
| | | catch (Exception e) |
| | | { |
| | | logger.traceException(e); |
| | | String str = pageRequest.getCookie().toHexString(); |
| | | LocalizableMessage msg = ERR_JEB_INVALID_PAGED_RESULTS_COOKIE.get(str); |
| | | throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, msg, e); |
| | | } |
| | | } |
| | | else |
| | | { |
| | | // Set the starting value to the suffix. |
| | | begin = suffix; |
| | | } |
| | | |
| | | ByteSequence startKey = begin; |
| | | |
| | | int lookthroughCount = 0; |
| | | int lookthroughLimit = searchOperation.getClientConnection().getLookthroughLimit(); |
| | | |
| | | try |
| | | { |
| | | Cursor cursor = storage.openCursor(dn2id.getName()); |
| | | try |
| | | { |
| | | // Initialize the cursor very close to the starting value. |
| | | boolean success = cursor.positionToKeyOrNext(startKey); |
| | | |
| | | // Step forward until we pass the ending value. |
| | | while (success) |
| | | { |
| | | if(lookthroughLimit > 0 && lookthroughCount > lookthroughLimit) |
| | | { |
| | | //Lookthrough limit exceeded |
| | | searchOperation.setResultCode(ResultCode.ADMIN_LIMIT_EXCEEDED); |
| | | searchOperation.appendErrorMessage( |
| | | NOTE_JEB_LOOKTHROUGH_LIMIT_EXCEEDED.get(lookthroughLimit)); |
| | | return; |
| | | } |
| | | int cmp = ByteSequence.COMPARATOR.compare(cursor.getKey(), end); |
| | | if (cmp >= 0) |
| | | { |
| | | // We have gone past the ending value. |
| | | break; |
| | | } |
| | | |
| | | // We have found a subordinate entry. |
| | | |
| | | EntryID entryID = new EntryID(cursor.getValue()); |
| | | |
| | | boolean isInScope = |
| | | searchScope != SearchScope.SINGLE_LEVEL |
| | | // Check if this entry is an immediate child. |
| | | || findDNKeyParent(cursor.getKey()) == baseDNKey.length(); |
| | | if (isInScope) |
| | | { |
| | | // Process the candidate entry. |
| | | final Entry entry = getEntry(entryID); |
| | | if (entry != null) |
| | | { |
| | | lookthroughCount++; |
| | | |
| | | if ((manageDsaIT || entry.getReferralURLs() == null) |
| | | && searchOperation.getFilter().matchesEntry(entry)) |
| | | { |
| | | if (pageRequest != null |
| | | && searchOperation.getEntriesSent() == pageRequest.getSize()) |
| | | { |
| | | // The current page is full. |
| | | // Set the cookie to remember where we were. |
| | | ByteString cookie = cursor.getKey(); |
| | | Control control = new PagedResultsControl(pageRequest.isCritical(), 0, cookie); |
| | | searchOperation.getResponseControls().add(control); |
| | | return; |
| | | } |
| | | |
| | | if (!searchOperation.returnEntry(entry, null)) |
| | | { |
| | | // We have been told to discontinue processing of the |
| | | // search. This could be due to size limit exceeded or |
| | | // operation cancelled. |
| | | return; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | searchOperation.checkIfCanceled(false); |
| | | |
| | | // Move to the next record. |
| | | success = cursor.next(); |
| | | } |
| | | } |
| | | finally |
| | | { |
| | | cursor.close(); |
| | | } |
| | | } |
| | | catch (StorageRuntimeException e) |
| | | { |
| | | logger.traceException(e); |
| | | } |
| | | |
| | | if (pageRequest != null) |
| | | { |
| | | // Indicate no more pages. |
| | | Control control = new PagedResultsControl(pageRequest.isCritical(), 0, null); |
| | | searchOperation.getResponseControls().add(control); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Returns the entry corresponding to the provided entryID. |
| | | * |
| | | * @param entryID |
| | | * the id of the entry to retrieve |
| | | * @return the entry corresponding to the provided entryID |
| | | * @throws DirectoryException |
| | | * If an error occurs retrieving the entry |
| | | */ |
| | | public Entry getEntry(EntryID entryID) throws DirectoryException |
| | | { |
| | | // Try the entry cache first. |
| | | final EntryCache entryCache = getEntryCache(); |
| | | final Entry cacheEntry = entryCache.getEntry(backend, entryID.longValue()); |
| | | if (cacheEntry != null) |
| | | { |
| | | return cacheEntry; |
| | | } |
| | | |
| | | final Entry entry = id2entry.get(null, entryID, false); |
| | | if (entry != null) |
| | | { |
| | | // Put the entry in the cache making sure not to overwrite a newer copy |
| | | // that may have been inserted since the time we read the cache. |
| | | entryCache.putEntryIfAbsent(entry, backend, entryID.longValue()); |
| | | } |
| | | return entry; |
| | | } |
| | | |
| | | /** |
| | | * We were able to obtain a set of candidate entry IDs for the |
| | | * search from the indexes. |
| | | * <p> |
| | | * Here we are relying on ID order to ensure children are returned |
| | | * after their parents. |
| | | * <ul> |
| | | * <li>Iterate through the candidate IDs |
| | | * <li>fetch entry by ID from cache or id2entry |
| | | * <li>put the entry in the cache if not present |
| | | * <li>discard entries that are not in scope |
| | | * <li>return entry if it matches the filter |
| | | * </ul> |
| | | * |
| | | * @param entryIDList The candidate entry IDs. |
| | | * @param candidatesAreInScope true if it is certain that every candidate |
| | | * entry is in the search scope. |
| | | * @param searchOperation The search operation. |
| | | * @param pageRequest A Paged Results control, or null if none. |
| | | * @throws DirectoryException If an error prevented the search from being |
| | | * processed. |
| | | */ |
| | | private void searchIndexed(EntryIDSet entryIDList, |
| | | boolean candidatesAreInScope, |
| | | SearchOperation searchOperation, |
| | | PagedResultsControl pageRequest) |
| | | throws DirectoryException, CanceledOperationException |
| | | { |
| | | SearchScope searchScope = searchOperation.getScope(); |
| | | DN aBaseDN = searchOperation.getBaseDN(); |
| | | boolean manageDsaIT = isManageDsaITOperation(searchOperation); |
| | | boolean continueSearch = true; |
| | | |
| | | // Set the starting value. |
| | | EntryID begin = null; |
| | | if (pageRequest != null && pageRequest.getCookie().length() != 0) |
| | | { |
| | | // The cookie contains the ID of the next entry to be returned. |
| | | try |
| | | { |
| | | begin = new EntryID(pageRequest.getCookie().toLong()); |
| | | } |
| | | catch (Exception e) |
| | | { |
| | | logger.traceException(e); |
| | | String str = pageRequest.getCookie().toHexString(); |
| | | LocalizableMessage msg = ERR_JEB_INVALID_PAGED_RESULTS_COOKIE.get(str); |
| | | throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, |
| | | msg, e); |
| | | } |
| | | } |
| | | else if (!manageDsaIT) |
| | | { |
| | | // Return any search result references. |
| | | continueSearch = dn2uri.returnSearchReferences(searchOperation); |
| | | } |
| | | |
| | | // Make sure the candidate list is smaller than the lookthrough limit |
| | | int lookthroughLimit = |
| | | searchOperation.getClientConnection().getLookthroughLimit(); |
| | | if(lookthroughLimit > 0 && entryIDList.size() > lookthroughLimit) |
| | | { |
| | | //Lookthrough limit exceeded |
| | | searchOperation.setResultCode(ResultCode.ADMIN_LIMIT_EXCEEDED); |
| | | searchOperation.appendErrorMessage( |
| | | NOTE_JEB_LOOKTHROUGH_LIMIT_EXCEEDED.get(lookthroughLimit)); |
| | | continueSearch = false; |
| | | } |
| | | |
| | | // Iterate through the index candidates. |
| | | if (continueSearch) |
| | | { |
| | | for (Iterator<EntryID> it = entryIDList.iterator(begin); it.hasNext();) |
| | | { |
| | | final EntryID id = it.next(); |
| | | |
| | | Entry entry; |
| | | try |
| | | { |
| | | entry = getEntry(id); |
| | | } |
| | | catch (Exception e) |
| | | { |
| | | logger.traceException(e); |
| | | continue; |
| | | } |
| | | |
| | | // Process the candidate entry. |
| | | if (entry != null) |
| | | { |
| | | // Filter the entry if it is in scope. |
| | | if (isInScope(candidatesAreInScope, searchScope, aBaseDN, entry) |
| | | && (manageDsaIT || entry.getReferralURLs() == null) |
| | | && searchOperation.getFilter().matchesEntry(entry)) |
| | | { |
| | | if (pageRequest != null |
| | | && searchOperation.getEntriesSent() == pageRequest.getSize()) |
| | | { |
| | | // The current page is full. |
| | | // Set the cookie to remember where we were. |
| | | ByteString cookie = id.toByteString(); |
| | | Control control = new PagedResultsControl(pageRequest.isCritical(), 0, cookie); |
| | | searchOperation.getResponseControls().add(control); |
| | | return; |
| | | } |
| | | |
| | | if (!searchOperation.returnEntry(entry, null)) |
| | | { |
| | | // We have been told to discontinue processing of the |
| | | // search. This could be due to size limit exceeded or |
| | | // operation cancelled. |
| | | break; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | searchOperation.checkIfCanceled(false); |
| | | } |
| | | |
| | | // Before we return success from the search we must ensure the base entry |
| | | // exists. However, if we have returned at least one entry or subordinate |
| | | // reference it implies the base does exist, so we can omit the check. |
| | | if (searchOperation.getEntriesSent() == 0 |
| | | && searchOperation.getReferencesSent() == 0) |
| | | { |
| | | // Fetch the base entry if it exists. |
| | | Entry baseEntry = fetchBaseEntry(aBaseDN, searchScope); |
| | | |
| | | if (!manageDsaIT) |
| | | { |
| | | dn2uri.checkTargetForReferral(baseEntry, searchScope); |
| | | } |
| | | } |
| | | |
| | | if (pageRequest != null) |
| | | { |
| | | // Indicate no more pages. |
| | | Control control = new PagedResultsControl(pageRequest.isCritical(), 0, null); |
| | | searchOperation.getResponseControls().add(control); |
| | | } |
| | | } |
| | | |
| | | private boolean isInScope(boolean candidatesAreInScope, SearchScope searchScope, DN aBaseDN, Entry entry) |
| | | { |
| | | DN entryDN = entry.getName(); |
| | | |
| | | if (candidatesAreInScope) |
| | | { |
| | | return true; |
| | | } |
| | | else if (searchScope == SearchScope.SINGLE_LEVEL) |
| | | { |
| | | // Check if this entry is an immediate child. |
| | | if (entryDN.size() == aBaseDN.size() + 1 |
| | | && entryDN.isDescendantOf(aBaseDN)) |
| | | { |
| | | return true; |
| | | } |
| | | } |
| | | else if (searchScope == SearchScope.WHOLE_SUBTREE) |
| | | { |
| | | if (entryDN.isDescendantOf(aBaseDN)) |
| | | { |
| | | return true; |
| | | } |
| | | } |
| | | else if (searchScope == SearchScope.SUBORDINATES |
| | | && entryDN.size() > aBaseDN.size() |
| | | && entryDN.isDescendantOf(aBaseDN)) |
| | | { |
| | | return true; |
| | | } |
| | | return false; |
| | | } |
| | | |
| | | /** |
| | | * Adds the provided entry to this database. This method must ensure that the |
| | | * entry is appropriate for the database and that no entry already exists with |
| | | * the same DN. The caller must hold a write lock on the DN of the provided |
| | | * entry. |
| | | * |
| | | * @param entry The entry to add to this database. |
| | | * @param addOperation The add operation with which the new entry is |
| | | * associated. This may be <CODE>null</CODE> for adds |
| | | * performed internally. |
| | | * @throws DirectoryException If a problem occurs while trying to add the |
| | | * entry. |
| | | * @throws StorageRuntimeException If an error occurs in the JE database. |
| | | * @throws CanceledOperationException if this operation should be cancelled. |
| | | */ |
| | | public void addEntry(final Entry entry, final AddOperation addOperation) |
| | | throws StorageRuntimeException, DirectoryException, CanceledOperationException |
| | | { |
| | | try |
| | | { |
| | | storage.update(new WriteOperation() |
| | | { |
| | | @Override |
| | | public void run(WriteableStorage txn) throws Exception |
| | | { |
| | | DN parentDN = getParentWithinBase(entry.getName()); |
| | | |
| | | try |
| | | { |
| | | // Check whether the entry already exists. |
| | | if (dn2id.get(txn, entry.getName(), false) != null) |
| | | { |
| | | throw new DirectoryException(ResultCode.ENTRY_ALREADY_EXISTS, ERR_JEB_ADD_ENTRY_ALREADY_EXISTS.get(entry |
| | | .getName())); |
| | | } |
| | | |
| | | // Check that the parent entry exists. |
| | | EntryID parentID = null; |
| | | if (parentDN != null) |
| | | { |
| | | // Check for referral entries above the target. |
| | | dn2uri.targetEntryReferrals(entry.getName(), null); |
| | | |
| | | // Read the parent ID from dn2id. |
| | | parentID = dn2id.get(txn, parentDN, false); |
| | | if (parentID == null) |
| | | { |
| | | LocalizableMessage message = ERR_JEB_ADD_NO_SUCH_OBJECT.get(entry.getName()); |
| | | DN matchedDN = getMatchedDN(baseDN); |
| | | throw new DirectoryException(ResultCode.NO_SUCH_OBJECT, message, matchedDN, null); |
| | | } |
| | | } |
| | | |
| | | EntryID entryID = rootContainer.getNextEntryID(); |
| | | |
| | | // Insert into dn2id. |
| | | if (!dn2id.insert(txn, entry.getName(), entryID)) |
| | | { |
| | | // Do not ever expect to come through here. |
| | | throw new DirectoryException(ResultCode.ENTRY_ALREADY_EXISTS, ERR_JEB_ADD_ENTRY_ALREADY_EXISTS.get(entry |
| | | .getName())); |
| | | } |
| | | |
| | | // Update the referral database for referral entries. |
| | | if (!dn2uri.addEntry(txn, entry)) |
| | | { |
| | | // Do not ever expect to come through here. |
| | | throw new DirectoryException(ResultCode.ENTRY_ALREADY_EXISTS, ERR_JEB_ADD_ENTRY_ALREADY_EXISTS.get(entry |
| | | .getName())); |
| | | } |
| | | |
| | | // Insert into id2entry. |
| | | if (!id2entry.insert(txn, entryID, entry)) |
| | | { |
| | | // Do not ever expect to come through here. |
| | | throw new DirectoryException(ResultCode.ENTRY_ALREADY_EXISTS, ERR_JEB_ADD_ENTRY_ALREADY_EXISTS.get(entry |
| | | .getName())); |
| | | } |
| | | |
| | | // Insert into the indexes, in index configuration order. |
| | | final IndexBuffer indexBuffer = new IndexBuffer(EntryContainer.this); |
| | | indexInsertEntry(indexBuffer, entry, entryID); |
| | | |
| | | // Insert into id2children and id2subtree. |
| | | // The database transaction locks on these records will be hotly |
| | | // contested so we do them last so as to hold the locks for the |
| | | // shortest duration. |
| | | if (parentDN != null) |
| | | { |
| | | final ByteString parentIDKeyBytes = parentID.toByteString(); |
| | | id2children.insertID(indexBuffer, parentIDKeyBytes, entryID); |
| | | id2subtree.insertID(indexBuffer, parentIDKeyBytes, entryID); |
| | | |
| | | // Iterate up through the superior entries, starting above the |
| | | // parent. |
| | | for (DN dn = getParentWithinBase(parentDN); dn != null; dn = getParentWithinBase(dn)) |
| | | { |
| | | // Read the ID from dn2id. |
| | | EntryID nodeID = dn2id.get(txn, dn, false); |
| | | if (nodeID == null) |
| | | { |
| | | throw new JebException(ERR_JEB_MISSING_DN2ID_RECORD.get(dn)); |
| | | } |
| | | |
| | | // Insert into id2subtree for this node. |
| | | id2subtree.insertID(indexBuffer, nodeID.toByteString(), entryID); |
| | | } |
| | | } |
| | | indexBuffer.flush(txn); |
| | | |
| | | if (addOperation != null) |
| | | { |
| | | // One last check before committing |
| | | addOperation.checkIfCanceled(true); |
| | | } |
| | | |
| | | // Commit the transaction. |
| | | EntryContainer.transactionCommit(txn); |
| | | |
| | | // Update the entry cache. |
| | | EntryCache<?> entryCache = DirectoryServer.getEntryCache(); |
| | | if (entryCache != null) |
| | | { |
| | | entryCache.putEntry(entry, backend, entryID.longValue()); |
| | | } |
| | | } |
| | | catch (StorageRuntimeException StorageRuntimeException) |
| | | { |
| | | EntryContainer.transactionAbort(txn); |
| | | throw StorageRuntimeException; |
| | | } |
| | | catch (DirectoryException directoryException) |
| | | { |
| | | EntryContainer.transactionAbort(txn); |
| | | throw directoryException; |
| | | } |
| | | catch (CanceledOperationException coe) |
| | | { |
| | | EntryContainer.transactionAbort(txn); |
| | | throw coe; |
| | | } |
| | | catch (Exception e) |
| | | { |
| | | EntryContainer.transactionAbort(txn); |
| | | |
| | | String msg = e.getMessage(); |
| | | if (msg == null) |
| | | { |
| | | msg = stackTraceToSingleLineString(e); |
| | | } |
| | | LocalizableMessage message = ERR_JEB_UNCHECKED_EXCEPTION.get(msg); |
| | | throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message, e); |
| | | } |
| | | } |
| | | }); |
| | | } |
| | | catch (Exception e) |
| | | { |
| | | throw new StorageRuntimeException(e); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Removes the specified entry from this database. This method must ensure |
| | | * that the entry exists and that it does not have any subordinate entries |
| | | * (unless the database supports a subtree delete operation and the client |
| | | * included the appropriate information in the request). The caller must hold |
| | | * a write lock on the provided entry DN. |
| | | * |
| | | * @param entryDN The DN of the entry to remove from this database. |
| | | * @param deleteOperation The delete operation with which this action is |
| | | * associated. This may be <CODE>null</CODE> for |
| | | * deletes performed internally. |
| | | * @throws DirectoryException If a problem occurs while trying to remove the |
| | | * entry. |
| | | * @throws StorageRuntimeException If an error occurs in the JE database. |
| | | * @throws CanceledOperationException if this operation should be cancelled. |
| | | */ |
| | | public void deleteEntry(final DN entryDN, final DeleteOperation deleteOperation) |
| | | throws DirectoryException, StorageRuntimeException, CanceledOperationException |
| | | { |
| | | try |
| | | { |
| | | storage.update(new WriteOperation() |
| | | { |
| | | @Override |
| | | public void run(WriteableStorage txn) throws Exception |
| | | { |
| | | final IndexBuffer indexBuffer = new IndexBuffer(EntryContainer.this); |
| | | |
| | | try |
| | | { |
| | | // Check for referral entries above the target entry. |
| | | dn2uri.targetEntryReferrals(entryDN, null); |
| | | |
| | | // Determine whether this is a subtree delete. |
| | | boolean isSubtreeDelete = |
| | | deleteOperation != null && deleteOperation.getRequestControl(SubtreeDeleteControl.DECODER) != null; |
| | | |
| | | /* |
| | | * We will iterate forwards through a range of the dn2id keys to |
| | | * find subordinates of the target entry from the top of the tree |
| | | * downwards. |
| | | */ |
| | | ByteString entryDNKey = dnToDNKey(entryDN, baseDN.size()); |
| | | ByteStringBuilder suffix = copyOf(entryDNKey); |
| | | ByteStringBuilder end = copyOf(entryDNKey); |
| | | |
| | | /* |
| | | * Set the ending value to a value of equal length but slightly |
| | | * greater than the suffix. |
| | | */ |
| | | suffix.append((byte) 0x00); |
| | | end.append((byte) 0x01); |
| | | |
| | | int subordinateEntriesDeleted = 0; |
| | | |
| | | ByteSequence startKey = suffix; |
| | | |
| | | CursorConfig cursorConfig = new CursorConfig(); |
| | | cursorConfig.setReadCommitted(true); |
| | | Cursor cursor = dn2id.openCursor(txn); |
| | | try |
| | | { |
| | | // Initialize the cursor very close to the starting value. |
| | | boolean success = cursor.positionToKeyOrNext(startKey); |
| | | |
| | | // Step forward until the key is greater than the starting value. |
| | | while (success && ByteSequence.COMPARATOR.compare(startKey, suffix) <= 0) |
| | | { |
| | | success = cursor.next(); |
| | | } |
| | | |
| | | // Step forward until we pass the ending value. |
| | | while (success) |
| | | { |
| | | int cmp = ByteSequence.COMPARATOR.compare(cursor.getKey(), end); |
| | | if (cmp >= 0) |
| | | { |
| | | // We have gone past the ending value. |
| | | break; |
| | | } |
| | | |
| | | // We have found a subordinate entry. |
| | | if (!isSubtreeDelete) |
| | | { |
| | | // The subtree delete control was not specified and |
| | | // the target entry is not a leaf. |
| | | throw new DirectoryException(ResultCode.NOT_ALLOWED_ON_NONLEAF, ERR_JEB_DELETE_NOT_ALLOWED_ON_NONLEAF |
| | | .get(entryDN)); |
| | | } |
| | | |
| | | /* |
| | | * Delete this entry which by now must be a leaf because we have |
| | | * been deleting from the bottom of the tree upwards. |
| | | */ |
| | | EntryID entryID = new EntryID(cursor.getValue()); |
| | | |
| | | // Invoke any subordinate delete plugins on the entry. |
| | | if (deleteOperation != null && !deleteOperation.isSynchronizationOperation()) |
| | | { |
| | | Entry subordinateEntry = id2entry.get(txn, entryID, false); |
| | | SubordinateDelete pluginResult = |
| | | getPluginConfigManager().invokeSubordinateDeletePlugins(deleteOperation, subordinateEntry); |
| | | |
| | | if (!pluginResult.continueProcessing()) |
| | | { |
| | | LocalizableMessage message = |
| | | ERR_JEB_DELETE_ABORTED_BY_SUBORDINATE_PLUGIN.get(dnFromDNKey(cursor.getKey(), getBaseDN())); |
| | | throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message); |
| | | } |
| | | } |
| | | |
| | | deleteEntry(txn, indexBuffer, true, entryDN, startKey, entryID); |
| | | subordinateEntriesDeleted++; |
| | | |
| | | if (deleteOperation != null) |
| | | { |
| | | deleteOperation.checkIfCanceled(false); |
| | | } |
| | | |
| | | // Get the next DN. |
| | | success = cursor.next(); |
| | | } |
| | | } |
| | | finally |
| | | { |
| | | cursor.close(); |
| | | } |
| | | |
| | | // draft-armijo-ldap-treedelete, 4.1 Tree Delete Semantics: |
| | | // The server MUST NOT chase referrals stored in the tree. If |
| | | // information about referrals is stored in this section of the |
| | | // tree, this pointer will be deleted. |
| | | boolean manageDsaIT = isSubtreeDelete || isManageDsaITOperation(deleteOperation); |
| | | deleteEntry(txn, indexBuffer, manageDsaIT, entryDN, null, null); |
| | | |
| | | indexBuffer.flush(txn); |
| | | |
| | | if (deleteOperation != null) |
| | | { |
| | | // One last check before committing |
| | | deleteOperation.checkIfCanceled(true); |
| | | } |
| | | |
| | | // Commit the transaction. |
| | | EntryContainer.transactionCommit(txn); |
| | | |
| | | if (isSubtreeDelete) |
| | | { |
| | | deleteOperation.addAdditionalLogItem(unquotedKeyValue(getClass(), "deletedEntries", |
| | | subordinateEntriesDeleted + 1)); |
| | | } |
| | | } |
| | | catch (StorageRuntimeException StorageRuntimeException) |
| | | { |
| | | EntryContainer.transactionAbort(txn); |
| | | throw StorageRuntimeException; |
| | | } |
| | | catch (DirectoryException directoryException) |
| | | { |
| | | EntryContainer.transactionAbort(txn); |
| | | throw directoryException; |
| | | } |
| | | catch (CanceledOperationException coe) |
| | | { |
| | | EntryContainer.transactionAbort(txn); |
| | | throw coe; |
| | | } |
| | | catch (Exception e) |
| | | { |
| | | EntryContainer.transactionAbort(txn); |
| | | |
| | | String msg = e.getMessage(); |
| | | if (msg == null) |
| | | { |
| | | msg = stackTraceToSingleLineString(e); |
| | | } |
| | | LocalizableMessage message = ERR_JEB_UNCHECKED_EXCEPTION.get(msg); |
| | | throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message, e); |
| | | } |
| | | } |
| | | }); |
| | | } |
| | | catch (Exception e) |
| | | { |
| | | throw new StorageRuntimeException(e); |
| | | } |
| | | } |
| | | |
| | | private ByteStringBuilder copyOf(ByteString bs) |
| | | { |
| | | ByteStringBuilder newBS = new ByteStringBuilder(bs.length() + 1); |
| | | newBS.append(bs); |
| | | return newBS; |
| | | } |
| | | |
| | | private void deleteEntry(WriteableStorage txn, |
| | | IndexBuffer indexBuffer, |
| | | boolean manageDsaIT, |
| | | DN targetDN, |
| | | ByteSequence leafDNKey, |
| | | EntryID leafID) |
| | | throws StorageRuntimeException, DirectoryException, JebException |
| | | { |
| | | if(leafID == null || leafDNKey == null) |
| | | { |
| | | // Read the entry ID from dn2id. |
| | | if(leafDNKey == null) |
| | | { |
| | | leafDNKey = dnToDNKey(targetDN, baseDN.size()); |
| | | } |
| | | ByteString value = dn2id.read(txn, leafDNKey, true); |
| | | if (value == null) |
| | | { |
| | | LocalizableMessage message = ERR_JEB_DELETE_NO_SUCH_OBJECT.get(leafDNKey); |
| | | DN matchedDN = getMatchedDN(baseDN); |
| | | throw new DirectoryException(ResultCode.NO_SUCH_OBJECT, message, matchedDN, null); |
| | | } |
| | | leafID = new EntryID(value); |
| | | } |
| | | |
| | | // Remove from dn2id. |
| | | if (!dn2id.delete(txn, leafDNKey)) |
| | | { |
| | | // Do not expect to ever come through here. |
| | | LocalizableMessage message = ERR_JEB_DELETE_NO_SUCH_OBJECT.get(leafDNKey); |
| | | DN matchedDN = getMatchedDN(baseDN); |
| | | throw new DirectoryException(ResultCode.NO_SUCH_OBJECT, message, matchedDN, null); |
| | | } |
| | | |
| | | // Check that the entry exists in id2entry and read its contents. |
| | | Entry entry = id2entry.get(txn, leafID, true); |
| | | if (entry == null) |
| | | { |
| | | throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), |
| | | ERR_JEB_MISSING_ID2ENTRY_RECORD.get(leafID)); |
| | | } |
| | | |
| | | if (!manageDsaIT) |
| | | { |
| | | dn2uri.checkTargetForReferral(entry, null); |
| | | } |
| | | |
| | | // Update the referral database. |
| | | dn2uri.deleteEntry(txn, entry); |
| | | |
| | | // Remove from id2entry. |
| | | if (!id2entry.remove(txn, leafID)) |
| | | { |
| | | throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), |
| | | ERR_JEB_MISSING_ID2ENTRY_RECORD.get(leafID)); |
| | | } |
| | | |
| | | // Remove from the indexes, in index config order. |
| | | indexRemoveEntry(indexBuffer, entry, leafID); |
| | | |
| | | // Remove the id2c and id2s records for this entry. |
| | | final ByteString leafIDKeyBytes = ByteString.valueOf(leafID.longValue()); |
| | | id2children.delete(indexBuffer, leafIDKeyBytes); |
| | | id2subtree.delete(indexBuffer, leafIDKeyBytes); |
| | | |
| | | // Iterate up through the superior entries from the target entry. |
| | | boolean isParent = true; |
| | | for (DN parentDN = getParentWithinBase(targetDN); parentDN != null; |
| | | parentDN = getParentWithinBase(parentDN)) |
| | | { |
| | | // Read the ID from dn2id. |
| | | EntryID parentID = dn2id.get(txn, parentDN, false); |
| | | if (parentID == null) |
| | | { |
| | | throw new JebException(ERR_JEB_MISSING_DN2ID_RECORD.get(parentDN)); |
| | | } |
| | | |
| | | ByteString parentIDBytes = ByteString.valueOf(parentID.longValue()); |
| | | // Remove from id2children. |
| | | if (isParent) |
| | | { |
| | | id2children.removeID(indexBuffer, parentIDBytes, leafID); |
| | | isParent = false; |
| | | } |
| | | id2subtree.removeID(indexBuffer, parentIDBytes, leafID); |
| | | } |
| | | |
| | | // Remove the entry from the entry cache. |
| | | EntryCache<?> entryCache = DirectoryServer.getEntryCache(); |
| | | if (entryCache != null) |
| | | { |
| | | entryCache.removeEntry(entry.getName()); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Indicates whether an entry with the specified DN exists. |
| | | * |
| | | * @param entryDN The DN of the entry for which to determine existence. |
| | | * |
| | | * @return <CODE>true</CODE> if the specified entry exists, |
| | | * or <CODE>false</CODE> if it does not. |
| | | * |
| | | * @throws DirectoryException If a problem occurs while trying to make the |
| | | * determination. |
| | | */ |
| | | public boolean entryExists(final DN entryDN) throws DirectoryException |
| | | { |
| | | // Try the entry cache first. |
| | | EntryCache<?> entryCache = DirectoryServer.getEntryCache(); |
| | | if (entryCache != null && entryCache.containsEntry(entryDN)) |
| | | { |
| | | return true; |
| | | } |
| | | |
| | | try |
| | | { |
| | | return storage.read(new ReadOperation<Boolean>() |
| | | { |
| | | @Override |
| | | public Boolean run(ReadableStorage txn) throws Exception |
| | | { |
| | | EntryID id = dn2id.get(null, entryDN, false); |
| | | return id != null; |
| | | } |
| | | }); |
| | | } |
| | | catch (Exception e) |
| | | { |
| | | logger.traceException(e); |
| | | return false; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Fetch an entry by DN, trying the entry cache first, then the database. |
| | | * Retrieves the requested entry, trying the entry cache first, |
| | | * then the database. Note that the caller must hold a read or write lock |
| | | * on the specified DN. |
| | | * |
| | | * @param entryDN The distinguished name of the entry to retrieve. |
| | | * @return The requested entry, or <CODE>null</CODE> if the entry does not |
| | | * exist. |
| | | * @throws DirectoryException If a problem occurs while trying to retrieve |
| | | * the entry. |
| | | * @throws StorageRuntimeException An error occurred during a database operation. |
| | | */ |
| | | public Entry getEntry(final DN entryDN) |
| | | throws StorageRuntimeException, DirectoryException |
| | | { |
| | | final EntryCache<?> entryCache = DirectoryServer.getEntryCache(); |
| | | Entry entry = null; |
| | | |
| | | // Try the entry cache first. |
| | | if (entryCache != null) |
| | | { |
| | | entry = entryCache.getEntry(entryDN); |
| | | } |
| | | |
| | | if (entry == null) |
| | | { |
| | | try |
| | | { |
| | | return storage.read(new ReadOperation<Entry>() |
| | | { |
| | | @Override |
| | | public Entry run(ReadableStorage txn) throws Exception |
| | | { |
| | | // Read dn2id. |
| | | EntryID entryID = dn2id.get(txn, entryDN, false); |
| | | if (entryID == null) |
| | | { |
| | | // The entryDN does not exist. |
| | | // Check for referral entries above the target entry. |
| | | dn2uri.targetEntryReferrals(entryDN, null); |
| | | return null; |
| | | } |
| | | |
| | | // Read id2entry. |
| | | Entry entry2 = id2entry.get(txn, entryID, false); |
| | | if (entry2 == null) |
| | | { |
| | | // The entryID does not exist. |
| | | throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), ERR_JEB_MISSING_ID2ENTRY_RECORD |
| | | .get(entryID)); |
| | | } |
| | | |
| | | // Put the entry in the cache making sure not to overwrite |
| | | // a newer copy that may have been inserted since the time |
| | | // we read the cache. |
| | | if (entryCache != null) |
| | | { |
| | | entryCache.putEntryIfAbsent(entry2, backend, entryID.longValue()); |
| | | } |
| | | return entry2; |
| | | } |
| | | }); |
| | | } |
| | | catch (Exception e) |
| | | { |
| | | throw new StorageRuntimeException(e); |
| | | } |
| | | } |
| | | |
| | | return entry; |
| | | } |
| | | |
| | | /** |
| | | * The simplest case of replacing an entry in which the entry DN has |
| | | * not changed. |
| | | * |
| | | * @param oldEntry The old contents of the entry |
| | | * @param newEntry The new contents of the entry |
| | | * @param modifyOperation The modify operation with which this action is |
| | | * associated. This may be <CODE>null</CODE> for |
| | | * modifications performed internally. |
| | | * @throws StorageRuntimeException If an error occurs in the JE database. |
| | | * @throws DirectoryException If a Directory Server error occurs. |
| | | * @throws CanceledOperationException if this operation should be cancelled. |
| | | */ |
| | | public void replaceEntry(final Entry oldEntry, final Entry newEntry, final ModifyOperation modifyOperation) |
| | | throws StorageRuntimeException, DirectoryException, CanceledOperationException |
| | | { |
| | | try |
| | | { |
| | | storage.update(new WriteOperation() |
| | | { |
| | | @Override |
| | | public void run(WriteableStorage txn) throws Exception |
| | | { |
| | | try |
| | | { |
| | | // Read dn2id. |
| | | EntryID entryID = dn2id.get(txn, newEntry.getName(), true); |
| | | if (entryID == null) |
| | | { |
| | | // The entry does not exist. |
| | | LocalizableMessage message = |
| | | ERR_JEB_MODIFY_NO_SUCH_OBJECT.get(newEntry.getName()); |
| | | DN matchedDN = getMatchedDN(baseDN); |
| | | throw new DirectoryException(ResultCode.NO_SUCH_OBJECT, |
| | | message, matchedDN, null); |
| | | } |
| | | |
| | | if (!isManageDsaITOperation(modifyOperation)) |
| | | { |
| | | // Check if the entry is a referral entry. |
| | | dn2uri.checkTargetForReferral(oldEntry, null); |
| | | } |
| | | |
| | | // Update the referral database. |
| | | if (modifyOperation != null) |
| | | { |
| | | // In this case we know from the operation what the modifications were. |
| | | List<Modification> mods = modifyOperation.getModifications(); |
| | | dn2uri.modifyEntry(txn, oldEntry, newEntry, mods); |
| | | } |
| | | else |
| | | { |
| | | dn2uri.replaceEntry(txn, oldEntry, newEntry); |
| | | } |
| | | |
| | | // Replace id2entry. |
| | | id2entry.put(txn, entryID, newEntry); |
| | | |
| | | // Update the indexes. |
| | | final IndexBuffer indexBuffer = new IndexBuffer(EntryContainer.this); |
| | | if (modifyOperation != null) |
| | | { |
| | | // In this case we know from the operation what the modifications were. |
| | | List<Modification> mods = modifyOperation.getModifications(); |
| | | indexModifications(indexBuffer, oldEntry, newEntry, entryID, mods); |
| | | } |
| | | else |
| | | { |
| | | // The most optimal would be to figure out what the modifications were. |
| | | indexRemoveEntry(indexBuffer, oldEntry, entryID); |
| | | indexInsertEntry(indexBuffer, newEntry, entryID); |
| | | } |
| | | |
| | | indexBuffer.flush(txn); |
| | | |
| | | if(modifyOperation != null) |
| | | { |
| | | // One last check before committing |
| | | modifyOperation.checkIfCanceled(true); |
| | | } |
| | | |
| | | // Commit the transaction. |
| | | EntryContainer.transactionCommit(txn); |
| | | |
| | | // Update the entry cache. |
| | | EntryCache<?> entryCache = DirectoryServer.getEntryCache(); |
| | | if (entryCache != null) |
| | | { |
| | | entryCache.putEntry(newEntry, backend, entryID.longValue()); |
| | | } |
| | | } |
| | | catch (StorageRuntimeException StorageRuntimeException) |
| | | { |
| | | EntryContainer.transactionAbort(txn); |
| | | throw StorageRuntimeException; |
| | | } |
| | | catch (DirectoryException directoryException) |
| | | { |
| | | EntryContainer.transactionAbort(txn); |
| | | throw directoryException; |
| | | } |
| | | catch (CanceledOperationException coe) |
| | | { |
| | | EntryContainer.transactionAbort(txn); |
| | | throw coe; |
| | | } |
| | | catch (Exception e) |
| | | { |
| | | EntryContainer.transactionAbort(txn); |
| | | |
| | | String msg = e.getMessage(); |
| | | if (msg == null) |
| | | { |
| | | msg = stackTraceToSingleLineString(e); |
| | | } |
| | | LocalizableMessage message = ERR_JEB_UNCHECKED_EXCEPTION.get(msg); |
| | | throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), |
| | | message, e); |
| | | } |
| | | } |
| | | }); |
| | | } |
| | | catch (Exception e) |
| | | { |
| | | throw new StorageRuntimeException(e); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Moves and/or renames the provided entry in this backend, altering any |
| | | * subordinate entries as necessary. This must ensure that an entry already |
| | | * exists with the provided current DN, and that no entry exists with the |
| | | * target DN of the provided entry. The caller must hold write locks on both |
| | | * the current DN and the new DN for the entry. |
| | | * |
| | | * @param currentDN The current DN of the entry to be replaced. |
| | | * @param entry The new content to use for the entry. |
| | | * @param modifyDNOperation The modify DN operation with which this action |
| | | * is associated. This may be <CODE>null</CODE> |
| | | * for modify DN operations performed internally. |
| | | * @throws DirectoryException |
| | | * If a problem occurs while trying to perform the rename. |
| | | * @throws CanceledOperationException |
| | | * If this backend noticed and reacted |
| | | * to a request to cancel or abandon the |
| | | * modify DN operation. |
| | | * @throws StorageRuntimeException If an error occurs in the JE database. |
| | | */ |
| | | public void renameEntry(final DN currentDN, final Entry entry, final ModifyDNOperation modifyDNOperation) |
| | | throws StorageRuntimeException, DirectoryException, CanceledOperationException |
| | | { |
| | | try |
| | | { |
| | | storage.update(new WriteOperation() |
| | | { |
| | | @Override |
| | | public void run(WriteableStorage txn) throws Exception |
| | | { |
| | | DN oldSuperiorDN = getParentWithinBase(currentDN); |
| | | DN newSuperiorDN = getParentWithinBase(entry.getName()); |
| | | |
| | | final boolean isApexEntryMoved; |
| | | if (oldSuperiorDN != null) |
| | | { |
| | | isApexEntryMoved = !oldSuperiorDN.equals(newSuperiorDN); |
| | | } |
| | | else if (newSuperiorDN != null) |
| | | { |
| | | isApexEntryMoved = !newSuperiorDN.equals(oldSuperiorDN); |
| | | } |
| | | else |
| | | { |
| | | isApexEntryMoved = false; |
| | | } |
| | | |
| | | IndexBuffer buffer = new IndexBuffer(EntryContainer.this); |
| | | |
| | | try |
| | | { |
| | | // Check whether the renamed entry already exists. |
| | | if (!currentDN.equals(entry.getName()) && dn2id.get(txn, entry.getName(), false) != null) |
| | | { |
| | | LocalizableMessage message = ERR_JEB_MODIFYDN_ALREADY_EXISTS.get(entry.getName()); |
| | | throw new DirectoryException(ResultCode.ENTRY_ALREADY_EXISTS, message); |
| | | } |
| | | |
| | | EntryID oldApexID = dn2id.get(txn, currentDN, false); |
| | | if (oldApexID == null) |
| | | { |
| | | // Check for referral entries above the target entry. |
| | | dn2uri.targetEntryReferrals(currentDN, null); |
| | | |
| | | LocalizableMessage message = ERR_JEB_MODIFYDN_NO_SUCH_OBJECT.get(currentDN); |
| | | DN matchedDN = getMatchedDN(baseDN); |
| | | throw new DirectoryException(ResultCode.NO_SUCH_OBJECT, message, matchedDN, null); |
| | | } |
| | | |
| | | Entry oldApexEntry = id2entry.get(txn, oldApexID, false); |
| | | if (oldApexEntry == null) |
| | | { |
| | | throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), ERR_JEB_MISSING_ID2ENTRY_RECORD |
| | | .get(oldApexID)); |
| | | } |
| | | |
| | | if (!isManageDsaITOperation(modifyDNOperation)) |
| | | { |
| | | dn2uri.checkTargetForReferral(oldApexEntry, null); |
| | | } |
| | | |
| | | EntryID newApexID = oldApexID; |
| | | if (newSuperiorDN != null && isApexEntryMoved) |
| | | { |
| | | /* |
| | | * We want to preserve the invariant that the ID of an entry is |
| | | * greater than its parent, since search results are returned in |
| | | * ID order. |
| | | */ |
| | | EntryID newSuperiorID = dn2id.get(txn, newSuperiorDN, false); |
| | | if (newSuperiorID == null) |
| | | { |
| | | LocalizableMessage msg = ERR_JEB_NEW_SUPERIOR_NO_SUCH_OBJECT.get(newSuperiorDN); |
| | | DN matchedDN = getMatchedDN(baseDN); |
| | | throw new DirectoryException(ResultCode.NO_SUCH_OBJECT, msg, matchedDN, null); |
| | | } |
| | | |
| | | if (newSuperiorID.compareTo(oldApexID) > 0) |
| | | { |
| | | // This move would break the above invariant so we must |
| | | // renumber every entry that moves. This is even more |
| | | // expensive since every entry has to be deleted from |
| | | // and added back into the attribute indexes. |
| | | newApexID = rootContainer.getNextEntryID(); |
| | | |
| | | if (logger.isTraceEnabled()) |
| | | { |
| | | logger.trace("Move of target entry requires renumbering" + "all entries in the subtree. " |
| | | + "Old DN: %s " + "New DN: %s " + "Old entry ID: %d " + "New entry ID: %d " |
| | | + "New Superior ID: %d" + oldApexEntry.getName(), entry.getName(), oldApexID.longValue(), |
| | | newApexID.longValue(), newSuperiorID.longValue()); |
| | | } |
| | | } |
| | | } |
| | | |
| | | MovedEntry head = new MovedEntry(null, null, false); |
| | | MovedEntry current = head; |
| | | // Move or rename the apex entry. |
| | | removeApexEntry(txn, buffer, oldSuperiorDN, oldApexID, newApexID, oldApexEntry, entry, isApexEntryMoved, |
| | | modifyDNOperation, current); |
| | | current = current.next; |
| | | |
| | | /* |
| | | * We will iterate forwards through a range of the dn2id keys to |
| | | * find subordinates of the target entry from the top of the tree |
| | | * downwards. |
| | | */ |
| | | ByteString currentDNKey = dnToDNKey(currentDN, baseDN.size()); |
| | | ByteStringBuilder suffix = copyOf(currentDNKey); |
| | | ByteStringBuilder end = copyOf(currentDNKey); |
| | | |
| | | /* |
| | | * Set the ending value to a value of equal length but slightly |
| | | * greater than the suffix. |
| | | */ |
| | | suffix.append((byte) 0x00); |
| | | end.append((byte) 0x01); |
| | | |
| | | ByteSequence startKey = suffix; |
| | | |
| | | Cursor cursor = txn.openCursor(dn2id.getName()); |
| | | try |
| | | { |
| | | // Initialize the cursor very close to the starting value. |
| | | boolean success = cursor.positionToKeyOrNext(startKey); |
| | | |
| | | // Step forward until the key is greater than the starting value. |
| | | while (success && ByteSequence.COMPARATOR.compare(cursor.getKey(), suffix) <= 0) |
| | | { |
| | | success = cursor.next(); |
| | | } |
| | | |
| | | // Step forward until we pass the ending value. |
| | | while (success) |
| | | { |
| | | int cmp = ByteSequence.COMPARATOR.compare(cursor.getKey(), end); |
| | | if (cmp >= 0) |
| | | { |
| | | // We have gone past the ending value. |
| | | break; |
| | | } |
| | | |
| | | // We have found a subordinate entry. |
| | | |
| | | EntryID oldID = new EntryID(cursor.getValue()); |
| | | Entry oldEntry = id2entry.get(txn, oldID, false); |
| | | |
| | | // Construct the new DN of the entry. |
| | | DN newDN = modDN(oldEntry.getName(), currentDN.size(), entry.getName()); |
| | | |
| | | // Assign a new entry ID if we are renumbering. |
| | | EntryID newID = oldID; |
| | | if (!newApexID.equals(oldApexID)) |
| | | { |
| | | newID = rootContainer.getNextEntryID(); |
| | | |
| | | if (logger.isTraceEnabled()) |
| | | { |
| | | logger.trace("Move of subordinate entry requires " + "renumbering. " + "Old DN: %s " |
| | | + "New DN: %s " + "Old entry ID: %d " + "New entry ID: %d", oldEntry.getName(), newDN, oldID |
| | | .longValue(), newID.longValue()); |
| | | } |
| | | } |
| | | |
| | | // Move this entry. |
| | | removeSubordinateEntry(txn, buffer, oldSuperiorDN, oldID, newID, oldEntry, newDN, isApexEntryMoved, |
| | | modifyDNOperation, current); |
| | | current = current.next; |
| | | |
| | | if (modifyDNOperation != null) |
| | | { |
| | | modifyDNOperation.checkIfCanceled(false); |
| | | } |
| | | |
| | | // Get the next DN. |
| | | success = cursor.next(); |
| | | } |
| | | } |
| | | finally |
| | | { |
| | | cursor.close(); |
| | | } |
| | | |
| | | // Set current to the first moved entry and null out the head. |
| | | // This will allow processed moved entries to be GCed. |
| | | current = head.next; |
| | | head = null; |
| | | while (current != null) |
| | | { |
| | | addRenamedEntry(txn, buffer, current.entryID, current.entry, isApexEntryMoved, current.renumbered, |
| | | modifyDNOperation); |
| | | current = current.next; |
| | | } |
| | | buffer.flush(txn); |
| | | |
| | | if (modifyDNOperation != null) |
| | | { |
| | | // One last check before committing |
| | | modifyDNOperation.checkIfCanceled(true); |
| | | } |
| | | |
| | | // Commit the transaction. |
| | | EntryContainer.transactionCommit(txn); |
| | | } |
| | | catch (StorageRuntimeException StorageRuntimeException) |
| | | { |
| | | EntryContainer.transactionAbort(txn); |
| | | throw StorageRuntimeException; |
| | | } |
| | | catch (DirectoryException directoryException) |
| | | { |
| | | EntryContainer.transactionAbort(txn); |
| | | throw directoryException; |
| | | } |
| | | catch (CanceledOperationException coe) |
| | | { |
| | | EntryContainer.transactionAbort(txn); |
| | | throw coe; |
| | | } |
| | | catch (Exception e) |
| | | { |
| | | EntryContainer.transactionAbort(txn); |
| | | |
| | | String msg = e.getMessage(); |
| | | if (msg == null) |
| | | { |
| | | msg = stackTraceToSingleLineString(e); |
| | | } |
| | | LocalizableMessage message = ERR_JEB_UNCHECKED_EXCEPTION.get(msg); |
| | | throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message, e); |
| | | } |
| | | } |
| | | }); |
| | | } |
| | | catch (Exception e) |
| | | { |
| | | throw new StorageRuntimeException(e); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Represents an renamed entry that was deleted from JE but yet to be added |
| | | * back. |
| | | */ |
| | | private static class MovedEntry |
| | | { |
| | | EntryID entryID; |
| | | Entry entry; |
| | | MovedEntry next; |
| | | boolean renumbered; |
| | | |
| | | private MovedEntry(EntryID entryID, Entry entry, boolean renumbered) |
| | | { |
| | | this.entryID = entryID; |
| | | this.entry = entry; |
| | | this.renumbered = renumbered; |
| | | } |
| | | } |
| | | |
| | | private void addRenamedEntry(WriteableStorage txn, IndexBuffer buffer, |
| | | EntryID newID, |
| | | Entry newEntry, |
| | | boolean isApexEntryMoved, |
| | | boolean renumbered, |
| | | ModifyDNOperation modifyDNOperation) |
| | | throws DirectoryException, StorageRuntimeException |
| | | { |
| | | if (!dn2id.insert(txn, newEntry.getName(), newID)) |
| | | { |
| | | LocalizableMessage message = ERR_JEB_MODIFYDN_ALREADY_EXISTS.get(newEntry.getName()); |
| | | throw new DirectoryException(ResultCode.ENTRY_ALREADY_EXISTS, message); |
| | | } |
| | | id2entry.put(txn, newID, newEntry); |
| | | dn2uri.addEntry(txn, newEntry); |
| | | |
| | | if (renumbered || modifyDNOperation == null) |
| | | { |
| | | // Reindex the entry with the new ID. |
| | | indexInsertEntry(buffer, newEntry, newID); |
| | | } |
| | | |
| | | // Add the new ID to id2children and id2subtree of new apex parent entry. |
| | | if(isApexEntryMoved) |
| | | { |
| | | boolean isParent = true; |
| | | for (DN dn = getParentWithinBase(newEntry.getName()); dn != null; |
| | | dn = getParentWithinBase(dn)) |
| | | { |
| | | EntryID parentID = dn2id.get(txn, dn, false); |
| | | ByteString parentIDKeyBytes = ByteString.valueOf(parentID.longValue()); |
| | | if(isParent) |
| | | { |
| | | id2children.insertID(buffer, parentIDKeyBytes, newID); |
| | | isParent = false; |
| | | } |
| | | id2subtree.insertID(buffer, parentIDKeyBytes, newID); |
| | | } |
| | | } |
| | | } |
| | | |
| | | private void removeApexEntry(WriteableStorage txn, IndexBuffer buffer, |
| | | DN oldSuperiorDN, |
| | | EntryID oldID, EntryID newID, |
| | | Entry oldEntry, Entry newEntry, |
| | | boolean isApexEntryMoved, |
| | | ModifyDNOperation modifyDNOperation, |
| | | MovedEntry tail) |
| | | throws DirectoryException, StorageRuntimeException |
| | | { |
| | | DN oldDN = oldEntry.getName(); |
| | | |
| | | // Remove the old DN from dn2id. |
| | | dn2id.remove(txn, oldDN); |
| | | |
| | | // Remove old ID from id2entry and put the new entry |
| | | // (old entry with new DN) in id2entry. |
| | | if (!newID.equals(oldID)) |
| | | { |
| | | id2entry.remove(txn, oldID); |
| | | } |
| | | |
| | | // Update any referral records. |
| | | dn2uri.deleteEntry(txn, oldEntry); |
| | | |
| | | tail.next = new MovedEntry(newID, newEntry, !newID.equals(oldID)); |
| | | |
| | | // Remove the old ID from id2children and id2subtree of |
| | | // the old apex parent entry. |
| | | if(oldSuperiorDN != null && isApexEntryMoved) |
| | | { |
| | | boolean isParent = true; |
| | | for (DN dn = oldSuperiorDN; dn != null; dn = getParentWithinBase(dn)) |
| | | { |
| | | EntryID parentID = dn2id.get(txn, dn, false); |
| | | ByteString parentIDKeyBytes = ByteString.valueOf(parentID.longValue()); |
| | | if(isParent) |
| | | { |
| | | id2children.removeID(buffer, parentIDKeyBytes, oldID); |
| | | isParent = false; |
| | | } |
| | | id2subtree.removeID(buffer, parentIDKeyBytes, oldID); |
| | | } |
| | | } |
| | | |
| | | if (!newID.equals(oldID) || modifyDNOperation == null) |
| | | { |
| | | // All the subordinates will be renumbered so we have to rebuild |
| | | // id2c and id2s with the new ID. |
| | | ByteString oldIDKeyBytes = ByteString.valueOf(oldID.longValue()); |
| | | id2children.delete(buffer, oldIDKeyBytes); |
| | | id2subtree.delete(buffer, oldIDKeyBytes); |
| | | |
| | | // Reindex the entry with the new ID. |
| | | indexRemoveEntry(buffer, oldEntry, oldID); |
| | | } |
| | | else |
| | | { |
| | | // Update the indexes if needed. |
| | | indexModifications(buffer, oldEntry, newEntry, oldID, |
| | | modifyDNOperation.getModifications()); |
| | | } |
| | | |
| | | // Remove the entry from the entry cache. |
| | | EntryCache<?> entryCache = DirectoryServer.getEntryCache(); |
| | | if (entryCache != null) |
| | | { |
| | | entryCache.removeEntry(oldDN); |
| | | } |
| | | } |
| | | |
| | | private void removeSubordinateEntry(WriteableStorage txn, IndexBuffer buffer, |
| | | DN oldSuperiorDN, |
| | | EntryID oldID, EntryID newID, |
| | | Entry oldEntry, DN newDN, |
| | | boolean isApexEntryMoved, |
| | | ModifyDNOperation modifyDNOperation, |
| | | MovedEntry tail) |
| | | throws DirectoryException, StorageRuntimeException |
| | | { |
| | | DN oldDN = oldEntry.getName(); |
| | | Entry newEntry = oldEntry.duplicate(false); |
| | | newEntry.setDN(newDN); |
| | | List<Modification> modifications = |
| | | Collections.unmodifiableList(new ArrayList<Modification>(0)); |
| | | |
| | | // Create a new entry that is a copy of the old entry but with the new DN. |
| | | // Also invoke any subordinate modify DN plugins on the entry. |
| | | // FIXME -- At the present time, we don't support subordinate modify DN |
| | | // plugins that make changes to subordinate entries and therefore |
| | | // provide an unmodifiable list for the modifications element. |
| | | // FIXME -- This will need to be updated appropriately if we decided that |
| | | // these plugins should be invoked for synchronization |
| | | // operations. |
| | | if (! modifyDNOperation.isSynchronizationOperation()) |
| | | { |
| | | SubordinateModifyDN pluginResult = |
| | | getPluginConfigManager().invokeSubordinateModifyDNPlugins( |
| | | modifyDNOperation, oldEntry, newEntry, modifications); |
| | | |
| | | if (!pluginResult.continueProcessing()) |
| | | { |
| | | throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), |
| | | ERR_JEB_MODIFYDN_ABORTED_BY_SUBORDINATE_PLUGIN.get(oldDN, newDN)); |
| | | } |
| | | |
| | | if (! modifications.isEmpty()) |
| | | { |
| | | LocalizableMessageBuilder invalidReason = new LocalizableMessageBuilder(); |
| | | if (! newEntry.conformsToSchema(null, false, false, false, |
| | | invalidReason)) |
| | | { |
| | | LocalizableMessage message = |
| | | ERR_JEB_MODIFYDN_ABORTED_BY_SUBORDINATE_SCHEMA_ERROR.get(oldDN, newDN, invalidReason); |
| | | throw new DirectoryException( |
| | | DirectoryServer.getServerErrorResultCode(), message); |
| | | } |
| | | } |
| | | } |
| | | |
| | | // Remove the old DN from dn2id. |
| | | dn2id.remove(txn, oldDN); |
| | | |
| | | // Remove old ID from id2entry and put the new entry |
| | | // (old entry with new DN) in id2entry. |
| | | if (!newID.equals(oldID)) |
| | | { |
| | | id2entry.remove(txn, oldID); |
| | | } |
| | | |
| | | // Update any referral records. |
| | | dn2uri.deleteEntry(txn, oldEntry); |
| | | |
| | | tail.next = new MovedEntry(newID, newEntry, !newID.equals(oldID)); |
| | | |
| | | if(isApexEntryMoved) |
| | | { |
| | | // Remove the old ID from id2subtree of old apex superior entries. |
| | | for (DN dn = oldSuperiorDN; dn != null; dn = getParentWithinBase(dn)) |
| | | { |
| | | EntryID parentID = dn2id.get(txn, dn, false); |
| | | ByteString parentIDKeyBytes = ByteString.valueOf(parentID.longValue()); |
| | | id2subtree.removeID(buffer, parentIDKeyBytes, oldID); |
| | | } |
| | | } |
| | | |
| | | if (!newID.equals(oldID)) |
| | | { |
| | | // All the subordinates will be renumbered so we have to rebuild |
| | | // id2c and id2s with the new ID. |
| | | ByteString oldIDKeyBytes = ByteString.valueOf(oldID.longValue()); |
| | | id2children.delete(buffer, oldIDKeyBytes); |
| | | id2subtree.delete(buffer, oldIDKeyBytes); |
| | | |
| | | // Reindex the entry with the new ID. |
| | | indexRemoveEntry(buffer, oldEntry, oldID); |
| | | } |
| | | else if (!modifications.isEmpty()) |
| | | { |
| | | // Update the indexes. |
| | | indexModifications(buffer, oldEntry, newEntry, oldID, modifications); |
| | | } |
| | | |
| | | // Remove the entry from the entry cache. |
| | | EntryCache<?> entryCache = DirectoryServer.getEntryCache(); |
| | | if (entryCache != null) |
| | | { |
| | | entryCache.removeEntry(oldDN); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Make a new DN for a subordinate entry of a renamed or moved entry. |
| | | * |
| | | * @param oldDN The current DN of the subordinate entry. |
| | | * @param oldSuffixLen The current DN length of the renamed or moved entry. |
| | | * @param newSuffixDN The new DN of the renamed or moved entry. |
| | | * @return The new DN of the subordinate entry. |
| | | */ |
| | | public static DN modDN(DN oldDN, int oldSuffixLen, DN newSuffixDN) |
| | | { |
| | | int oldDNNumComponents = oldDN.size(); |
| | | int oldDNKeepComponents = oldDNNumComponents - oldSuffixLen; |
| | | int newSuffixDNComponents = newSuffixDN.size(); |
| | | |
| | | RDN[] newDNComponents = new RDN[oldDNKeepComponents+newSuffixDNComponents]; |
| | | for (int i=0; i < oldDNKeepComponents; i++) |
| | | { |
| | | newDNComponents[i] = oldDN.getRDN(i); |
| | | } |
| | | |
| | | for (int i=oldDNKeepComponents, j=0; j < newSuffixDNComponents; i++,j++) |
| | | { |
| | | newDNComponents[i] = newSuffixDN.getRDN(j); |
| | | } |
| | | |
| | | return new DN(newDNComponents); |
| | | } |
| | | |
| | | /** |
| | | * Insert a new entry into the attribute indexes. |
| | | * |
| | | * @param buffer The index buffer used to buffer up the index changes. |
| | | * @param entry The entry to be inserted into the indexes. |
| | | * @param entryID The ID of the entry to be inserted into the indexes. |
| | | * @throws StorageRuntimeException If an error occurs in the JE database. |
| | | * @throws DirectoryException If a Directory Server error occurs. |
| | | */ |
| | | private void indexInsertEntry(IndexBuffer buffer, Entry entry, EntryID entryID) |
| | | throws StorageRuntimeException, DirectoryException |
| | | { |
| | | for (AttributeIndex index : attrIndexMap.values()) |
| | | { |
| | | index.addEntry(buffer, entryID, entry); |
| | | } |
| | | |
| | | for (VLVIndex vlvIndex : vlvIndexMap.values()) |
| | | { |
| | | vlvIndex.addEntry(buffer, entryID, entry); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Remove an entry from the attribute indexes. |
| | | * |
| | | * @param buffer The index buffer used to buffer up the index changes. |
| | | * @param entry The entry to be removed from the indexes. |
| | | * @param entryID The ID of the entry to be removed from the indexes. |
| | | * @throws StorageRuntimeException If an error occurs in the JE database. |
| | | * @throws DirectoryException If a Directory Server error occurs. |
| | | */ |
| | | private void indexRemoveEntry(IndexBuffer buffer, Entry entry, EntryID entryID) |
| | | throws StorageRuntimeException, DirectoryException |
| | | { |
| | | for (AttributeIndex index : attrIndexMap.values()) |
| | | { |
| | | index.removeEntry(buffer, entryID, entry); |
| | | } |
| | | |
| | | for (VLVIndex vlvIndex : vlvIndexMap.values()) |
| | | { |
| | | vlvIndex.removeEntry(buffer, entryID, entry); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Update the attribute indexes to reflect the changes to the |
| | | * attributes of an entry resulting from a sequence of modifications. |
| | | * |
| | | * @param buffer The index buffer used to buffer up the index changes. |
| | | * @param oldEntry The contents of the entry before the change. |
| | | * @param newEntry The contents of the entry after the change. |
| | | * @param entryID The ID of the entry that was changed. |
| | | * @param mods The sequence of modifications made to the entry. |
| | | * @throws StorageRuntimeException If an error occurs in the JE database. |
| | | * @throws DirectoryException If a Directory Server error occurs. |
| | | */ |
| | | private void indexModifications(IndexBuffer buffer, Entry oldEntry, Entry newEntry, |
| | | EntryID entryID, List<Modification> mods) |
| | | throws StorageRuntimeException, DirectoryException |
| | | { |
| | | // Process in index configuration order. |
| | | for (AttributeIndex index : attrIndexMap.values()) |
| | | { |
| | | // Check whether any modifications apply to this indexed attribute. |
| | | if (isAttributeModified(index, mods)) |
| | | { |
| | | index.modifyEntry(buffer, entryID, oldEntry, newEntry, mods); |
| | | } |
| | | } |
| | | |
| | | for(VLVIndex vlvIndex : vlvIndexMap.values()) |
| | | { |
| | | vlvIndex.modifyEntry(buffer, entryID, oldEntry, newEntry, mods); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Get a count of the number of entries stored in this entry container. |
| | | * |
| | | * @return The number of entries stored in this entry container. |
| | | * @throws StorageRuntimeException If an error occurs in the JE database. |
| | | */ |
| | | @Override |
| | | public long getEntryCount() throws StorageRuntimeException |
| | | { |
| | | EntryID entryID = dn2id.get(null, baseDN, false); |
| | | if (entryID != null) |
| | | { |
| | | ByteString key = entryIDToDatabase(entryID.longValue()); |
| | | EntryIDSet entryIDSet = id2subtree.readKey(key, null); |
| | | |
| | | long count = entryIDSet.size(); |
| | | if(count != Long.MAX_VALUE) |
| | | { |
| | | // Add the base entry itself |
| | | return ++count; |
| | | } |
| | | else |
| | | { |
| | | // The count is not maintained. Fall back to the slow method |
| | | return id2entry.getRecordCount(); |
| | | } |
| | | } |
| | | else |
| | | { |
| | | // Base entry doesn't not exist so this entry container |
| | | // must not have any entries |
| | | return 0; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Get the number of values for which the entry limit has been exceeded |
| | | * since the entry container was opened. |
| | | * @return The number of values for which the entry limit has been exceeded. |
| | | */ |
| | | public int getEntryLimitExceededCount() |
| | | { |
| | | int count = 0; |
| | | count += id2children.getEntryLimitExceededCount(); |
| | | count += id2subtree.getEntryLimitExceededCount(); |
| | | for (AttributeIndex index : attrIndexMap.values()) |
| | | { |
| | | count += index.getEntryLimitExceededCount(); |
| | | } |
| | | return count; |
| | | } |
| | | |
| | | |
| | | /** |
| | | * Get a list of the databases opened by the entryContainer. |
| | | * @param dbList A list of database containers. |
| | | */ |
| | | public void listDatabases(List<DatabaseContainer> dbList) |
| | | { |
| | | dbList.add(dn2id); |
| | | dbList.add(id2entry); |
| | | dbList.add(dn2uri); |
| | | if (config.isSubordinateIndexesEnabled()) |
| | | { |
| | | dbList.add(id2children); |
| | | dbList.add(id2subtree); |
| | | } |
| | | dbList.add(state); |
| | | |
| | | for(AttributeIndex index : attrIndexMap.values()) |
| | | { |
| | | index.listDatabases(dbList); |
| | | } |
| | | |
| | | dbList.addAll(vlvIndexMap.values()); |
| | | } |
| | | |
| | | /** |
| | | * Determine whether the provided operation has the ManageDsaIT request |
| | | * control. |
| | | * @param operation The operation for which the determination is to be made. |
| | | * @return true if the operation has the ManageDsaIT request control, or false |
| | | * if not. |
| | | */ |
| | | private static boolean isManageDsaITOperation(Operation operation) |
| | | { |
| | | if(operation != null) |
| | | { |
| | | List<Control> controls = operation.getRequestControls(); |
| | | if (controls != null) |
| | | { |
| | | for (Control control : controls) |
| | | { |
| | | if (ServerConstants.OID_MANAGE_DSAIT_CONTROL.equals(control.getOID())) |
| | | { |
| | | return true; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | return false; |
| | | } |
| | | |
| | | /** |
| | | * Begin a leaf transaction using the default configuration. |
| | | * Provides assertion debug logging. |
| | | * @return A JE transaction handle. |
| | | * @throws StorageRuntimeException If an error occurs while attempting to begin |
| | | * a new transaction. |
| | | */ |
| | | public Transaction beginTransaction() |
| | | throws StorageRuntimeException |
| | | { |
| | | Transaction parentTxn = null; |
| | | TransactionConfig txnConfig = null; |
| | | Transaction txn = storage.beginTransaction(parentTxn, txnConfig); |
| | | if (logger.isTraceEnabled()) |
| | | { |
| | | logger.trace("beginTransaction", "begin txnid=" + txn.getId()); |
| | | } |
| | | return txn; |
| | | } |
| | | |
| | | /** |
| | | * Commit a transaction. |
| | | * Provides assertion debug logging. |
| | | * @param txn The JE transaction handle. |
| | | * @throws StorageRuntimeException If an error occurs while attempting to commit |
| | | * the transaction. |
| | | */ |
| | | public static void transactionCommit(WriteableStorage txn) |
| | | throws StorageRuntimeException |
| | | { |
| | | if (txn != null) |
| | | { |
| | | txn.commit(); |
| | | if (logger.isTraceEnabled()) |
| | | { |
| | | logger.trace("commit txnid=%d", txn.getId()); |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Abort a transaction. |
| | | * Provides assertion debug logging. |
| | | * @param txn The JE transaction handle. |
| | | * @throws StorageRuntimeException If an error occurs while attempting to abort the |
| | | * transaction. |
| | | */ |
| | | public static void transactionAbort(WriteableStorage txn) |
| | | throws StorageRuntimeException |
| | | { |
| | | if (txn != null) |
| | | { |
| | | txn.abort(); |
| | | if (logger.isTraceEnabled()) |
| | | { |
| | | logger.trace("abort txnid=%d", txn.getId()); |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Delete this entry container from disk. The entry container should be |
| | | * closed before calling this method. |
| | | * |
| | | * @throws StorageRuntimeException If an error occurs while removing the entry |
| | | * container. |
| | | */ |
| | | public void delete() throws StorageRuntimeException |
| | | { |
| | | List<DatabaseContainer> databases = new ArrayList<DatabaseContainer>(); |
| | | listDatabases(databases); |
| | | |
| | | if(storage.getConfig().getTransactional()) |
| | | { |
| | | Transaction txn = beginTransaction(); |
| | | |
| | | try |
| | | { |
| | | for(DatabaseContainer db : databases) |
| | | { |
| | | storage.removeDatabase(txn, db.getName()); |
| | | } |
| | | |
| | | transactionCommit(txn); |
| | | } |
| | | catch(StorageRuntimeException de) |
| | | { |
| | | transactionAbort(txn); |
| | | throw de; |
| | | } |
| | | } |
| | | else |
| | | { |
| | | for(DatabaseContainer db : databases) |
| | | { |
| | | storage.removeDatabase(null, db.getName()); |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Remove a database from disk. |
| | | * |
| | | * @param database The database container to remove. |
| | | * @throws StorageRuntimeException If an error occurs while attempting to delete the |
| | | * database. |
| | | */ |
| | | public void deleteDatabase(DatabaseContainer database) |
| | | throws StorageRuntimeException |
| | | { |
| | | if(database == state) |
| | | { |
| | | // The state database can not be removed individually. |
| | | return; |
| | | } |
| | | |
| | | database.close(); |
| | | if(storage.getConfig().getTransactional()) |
| | | { |
| | | Transaction txn = beginTransaction(); |
| | | try |
| | | { |
| | | storage.removeDatabase(txn, database.getName()); |
| | | if(database instanceof Index) |
| | | { |
| | | state.removeIndexTrustState(txn, database); |
| | | } |
| | | transactionCommit(txn); |
| | | } |
| | | catch(StorageRuntimeException de) |
| | | { |
| | | transactionAbort(txn); |
| | | throw de; |
| | | } |
| | | } |
| | | else |
| | | { |
| | | storage.removeDatabase(null, database.getName()); |
| | | if(database instanceof Index) |
| | | { |
| | | state.removeIndexTrustState(null, database); |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Removes a attribute index from disk. |
| | | * |
| | | * @param attributeIndex The attribute index to remove. |
| | | * @throws StorageRuntimeException If an JE database error occurs while attempting |
| | | * to delete the index. |
| | | */ |
| | | private void deleteAttributeIndex(AttributeIndex attributeIndex) |
| | | throws StorageRuntimeException |
| | | { |
| | | attributeIndex.close(); |
| | | Transaction txn = storage.getConfig().getTransactional() |
| | | ? beginTransaction() : null; |
| | | try |
| | | { |
| | | for (Index index : attributeIndex.getAllIndexes()) |
| | | { |
| | | storage.removeDatabase(txn, index.getName()); |
| | | state.removeIndexTrustState(txn, index); |
| | | } |
| | | if (txn != null) |
| | | { |
| | | transactionCommit(txn); |
| | | } |
| | | } |
| | | catch(StorageRuntimeException de) |
| | | { |
| | | if (txn != null) |
| | | { |
| | | transactionAbort(txn); |
| | | } |
| | | throw de; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * This method constructs a container name from a base DN. Only alphanumeric |
| | | * characters are preserved, all other characters are replaced with an |
| | | * underscore. |
| | | * |
| | | * @return The container name for the base DN. |
| | | */ |
| | | public TreeName getDatabasePrefix() |
| | | { |
| | | return databasePrefix; |
| | | } |
| | | |
| | | /** |
| | | * Sets a new database prefix for this entry container and rename all |
| | | * existing databases in use by this entry container. |
| | | * |
| | | * @param newDatabasePrefix The new database prefix to use. |
| | | * @throws StorageRuntimeException If an error occurs in the JE database. |
| | | * @throws JebException If an error occurs in the JE backend. |
| | | */ |
| | | public void setDatabasePrefix(String newDatabasePrefix) |
| | | throws StorageRuntimeException, JebException |
| | | |
| | | { |
| | | List<DatabaseContainer> databases = new ArrayList<DatabaseContainer>(); |
| | | listDatabases(databases); |
| | | |
| | | TreeName newDbPrefix = preparePrefix(newDatabasePrefix); |
| | | |
| | | // close the containers. |
| | | for(DatabaseContainer db : databases) |
| | | { |
| | | db.close(); |
| | | } |
| | | |
| | | try |
| | | { |
| | | if(storage.getConfig().getTransactional()) |
| | | { |
| | | //Rename under transaction |
| | | Transaction txn = beginTransaction(); |
| | | try |
| | | { |
| | | for(DatabaseContainer db : databases) |
| | | { |
| | | TreeName oldName = db.getName(); |
| | | String newName = oldName.replace(databasePrefix, newDbPrefix); |
| | | storage.renameDatabase(txn, oldName, newName); |
| | | } |
| | | |
| | | transactionCommit(txn); |
| | | |
| | | for(DatabaseContainer db : databases) |
| | | { |
| | | TreeName oldName = db.getName(); |
| | | String newName = oldName.replace(databasePrefix, newDbPrefix); |
| | | db.setName(newName); |
| | | } |
| | | |
| | | // Update the prefix. |
| | | this.databasePrefix = newDbPrefix; |
| | | } |
| | | catch(Exception e) |
| | | { |
| | | transactionAbort(txn); |
| | | |
| | | String msg = e.getMessage(); |
| | | if (msg == null) |
| | | { |
| | | msg = stackTraceToSingleLineString(e); |
| | | } |
| | | LocalizableMessage message = ERR_JEB_UNCHECKED_EXCEPTION.get(msg); |
| | | throw new JebException(message, e); |
| | | } |
| | | } |
| | | else |
| | | { |
| | | for(DatabaseContainer db : databases) |
| | | { |
| | | TreeName oldName = db.getName(); |
| | | String newName = oldName.replace(databasePrefix, newDbPrefix); |
| | | storage.renameDatabase(null, oldName, newName); |
| | | db.setName(newName); |
| | | } |
| | | |
| | | // Update the prefix. |
| | | this.databasePrefix = newDbPrefix; |
| | | } |
| | | } |
| | | finally |
| | | { |
| | | // Open the containers backup. |
| | | for(DatabaseContainer db : databases) |
| | | { |
| | | db.open(); |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public DN getBaseDN() |
| | | { |
| | | return baseDN; |
| | | } |
| | | |
| | | /** |
| | | * Get the parent of a DN in the scope of the base DN. |
| | | * |
| | | * @param dn A DN which is in the scope of the base DN. |
| | | * @return The parent DN, or null if the given DN is the base DN. |
| | | */ |
| | | public DN getParentWithinBase(DN dn) |
| | | { |
| | | if (dn.equals(baseDN)) |
| | | { |
| | | return null; |
| | | } |
| | | return dn.parent(); |
| | | } |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public boolean isConfigurationChangeAcceptable( |
| | | LocalDBBackendCfg cfg, List<LocalizableMessage> unacceptableReasons) |
| | | { |
| | | // This is always true because only all config attributes used |
| | | // by the entry container should be validated by the admin framework. |
| | | return true; |
| | | } |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public ConfigChangeResult applyConfigurationChange(LocalDBBackendCfg cfg) |
| | | { |
| | | boolean adminActionRequired = false; |
| | | ArrayList<LocalizableMessage> messages = new ArrayList<LocalizableMessage>(); |
| | | |
| | | exclusiveLock.lock(); |
| | | try |
| | | { |
| | | if (config.isSubordinateIndexesEnabled() != cfg.isSubordinateIndexesEnabled()) |
| | | { |
| | | if (cfg.isSubordinateIndexesEnabled()) |
| | | { |
| | | // Re-enabling subordinate indexes. |
| | | openSubordinateIndexes(); |
| | | } |
| | | else |
| | | { |
| | | // Disabling subordinate indexes. Use a null index and ensure that |
| | | // future attempts to use the real indexes will fail. |
| | | id2children.close(); |
| | | id2children = new NullIndex(databasePrefix.child(ID2CHILDREN_DATABASE_NAME), |
| | | new ID2CIndexer(), state, storage, this); |
| | | state.putIndexTrustState(null, id2children, false); |
| | | id2children.open(); // No-op |
| | | |
| | | id2subtree.close(); |
| | | id2subtree = new NullIndex(databasePrefix.child(ID2SUBTREE_DATABASE_NAME), |
| | | new ID2SIndexer(), state, storage, this); |
| | | state.putIndexTrustState(null, id2subtree, false); |
| | | id2subtree.open(); // No-op |
| | | |
| | | logger.info(NOTE_JEB_SUBORDINATE_INDEXES_DISABLED, cfg.getBackendId()); |
| | | } |
| | | } |
| | | |
| | | if (config.getIndexEntryLimit() != cfg.getIndexEntryLimit()) |
| | | { |
| | | if (id2children.setIndexEntryLimit(cfg.getIndexEntryLimit())) |
| | | { |
| | | adminActionRequired = true; |
| | | messages.add(NOTE_JEB_CONFIG_INDEX_ENTRY_LIMIT_REQUIRES_REBUILD.get(id2children.getName())); |
| | | } |
| | | |
| | | if (id2subtree.setIndexEntryLimit(cfg.getIndexEntryLimit())) |
| | | { |
| | | adminActionRequired = true; |
| | | messages.add(NOTE_JEB_CONFIG_INDEX_ENTRY_LIMIT_REQUIRES_REBUILD.get(id2subtree.getName())); |
| | | } |
| | | } |
| | | |
| | | DataConfig entryDataConfig = new DataConfig(cfg.isEntriesCompressed(), |
| | | cfg.isCompactEncoding(), rootContainer.getCompressedSchema()); |
| | | id2entry.setDataConfig(entryDataConfig); |
| | | |
| | | this.config = cfg; |
| | | } |
| | | catch (StorageRuntimeException e) |
| | | { |
| | | messages.add(LocalizableMessage.raw(stackTraceToSingleLineString(e))); |
| | | return new ConfigChangeResult(DirectoryServer.getServerErrorResultCode(), |
| | | false, messages); |
| | | } |
| | | finally |
| | | { |
| | | exclusiveLock.unlock(); |
| | | } |
| | | |
| | | return new ConfigChangeResult(ResultCode.SUCCESS, adminActionRequired, messages); |
| | | } |
| | | |
| | | /** |
| | | * Get the environment config of the JE environment used in this entry |
| | | * container. |
| | | * |
| | | * @return The environment config of the JE environment. |
| | | * @throws StorageRuntimeException If an error occurs while retrieving the |
| | | * configuration object. |
| | | */ |
| | | public EnvironmentConfig getEnvironmentConfig() throws StorageRuntimeException |
| | | { |
| | | return storage.getConfig(); |
| | | } |
| | | |
| | | /** |
| | | * Clear the contents of this entry container. |
| | | * |
| | | * @return The number of records deleted. |
| | | * @throws StorageRuntimeException If an error occurs while removing the entry |
| | | * container. |
| | | */ |
| | | public long clear() throws StorageRuntimeException |
| | | { |
| | | List<DatabaseContainer> databases = new ArrayList<DatabaseContainer>(); |
| | | listDatabases(databases); |
| | | long count = 0; |
| | | |
| | | for(DatabaseContainer db : databases) |
| | | { |
| | | db.close(); |
| | | } |
| | | try |
| | | { |
| | | if(storage.getConfig().getTransactional()) |
| | | { |
| | | Transaction txn = beginTransaction(); |
| | | |
| | | try |
| | | { |
| | | for(DatabaseContainer db : databases) |
| | | { |
| | | count += storage.truncateDatabase(txn, db.getName(), true); |
| | | } |
| | | |
| | | transactionCommit(txn); |
| | | } |
| | | catch(StorageRuntimeException de) |
| | | { |
| | | transactionAbort(txn); |
| | | throw de; |
| | | } |
| | | } |
| | | else |
| | | { |
| | | for(DatabaseContainer db : databases) |
| | | { |
| | | count += storage.truncateDatabase(null, db.getName(), true); |
| | | } |
| | | } |
| | | } |
| | | finally |
| | | { |
| | | for(DatabaseContainer db : databases) |
| | | { |
| | | db.open(); |
| | | } |
| | | |
| | | Transaction txn = null; |
| | | try |
| | | { |
| | | if(storage.getConfig().getTransactional()) { |
| | | txn = beginTransaction(); |
| | | } |
| | | for(DatabaseContainer db : databases) |
| | | { |
| | | if (db instanceof Index) |
| | | { |
| | | Index index = (Index)db; |
| | | index.setTrusted(txn, true); |
| | | } |
| | | } |
| | | if(storage.getConfig().getTransactional()) { |
| | | transactionCommit(txn); |
| | | } |
| | | } |
| | | catch(Exception de) |
| | | { |
| | | logger.traceException(de); |
| | | |
| | | // This is mainly used during the unit tests, so it's not essential. |
| | | try |
| | | { |
| | | if (txn != null) |
| | | { |
| | | transactionAbort(txn); |
| | | } |
| | | } |
| | | catch (Exception e) |
| | | { |
| | | logger.traceException(de); |
| | | } |
| | | } |
| | | } |
| | | |
| | | return count; |
| | | } |
| | | |
| | | /** |
| | | * Clear the contents for a database from disk. |
| | | * |
| | | * @param database The database to clear. |
| | | * @throws StorageRuntimeException if a JE database error occurs. |
| | | */ |
| | | public void clearDatabase(DatabaseContainer database) |
| | | throws StorageRuntimeException |
| | | { |
| | | database.close(); |
| | | try |
| | | { |
| | | if(storage.getConfig().getTransactional()) |
| | | { |
| | | Transaction txn = beginTransaction(); |
| | | try |
| | | { |
| | | storage.removeDatabase(txn, database.getName()); |
| | | transactionCommit(txn); |
| | | } |
| | | catch(StorageRuntimeException de) |
| | | { |
| | | transactionAbort(txn); |
| | | throw de; |
| | | } |
| | | } |
| | | else |
| | | { |
| | | storage.removeDatabase(null, database.getName()); |
| | | } |
| | | } |
| | | finally |
| | | { |
| | | database.open(); |
| | | } |
| | | if(logger.isTraceEnabled()) |
| | | { |
| | | logger.trace("Cleared the database %s", database.getName()); |
| | | } |
| | | } |
| | | |
| | | |
| | | /** |
| | | * Finds an existing entry whose DN is the closest ancestor of a given baseDN. |
| | | * |
| | | * @param baseDN the DN for which we are searching a matched DN. |
| | | * @return the DN of the closest ancestor of the baseDN. |
| | | * @throws DirectoryException If an error prevented the check of an |
| | | * existing entry from being performed. |
| | | */ |
| | | private DN getMatchedDN(DN baseDN) throws DirectoryException |
| | | { |
| | | DN parentDN = baseDN.getParentDNInSuffix(); |
| | | while (parentDN != null && parentDN.isDescendantOf(getBaseDN())) |
| | | { |
| | | if (entryExists(parentDN)) |
| | | { |
| | | return parentDN; |
| | | } |
| | | parentDN = parentDN.getParentDNInSuffix(); |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | /** |
| | | * Opens the id2children and id2subtree indexes. |
| | | */ |
| | | private void openSubordinateIndexes() |
| | | { |
| | | id2children = newIndex(ID2CHILDREN_DATABASE_NAME, new ID2CIndexer()); |
| | | id2subtree = newIndex(ID2SUBTREE_DATABASE_NAME, new ID2SIndexer()); |
| | | } |
| | | |
| | | private Index newIndex(String name, Indexer indexer) |
| | | { |
| | | final Index index = new Index(databasePrefix.child(name), |
| | | indexer, state, config.getIndexEntryLimit(), 0, true, storage, this); |
| | | index.open(); |
| | | if (!index.isTrusted()) |
| | | { |
| | | logger.info(NOTE_JEB_INDEX_ADD_REQUIRES_REBUILD, index.getName()); |
| | | } |
| | | return index; |
| | | } |
| | | |
| | | /** |
| | | * Creates a new index for an attribute. |
| | | * |
| | | * @param indexName the name to give to the new index |
| | | * @param indexer the indexer to use when inserting data into the index |
| | | * @param indexEntryLimit the index entry limit |
| | | * @return a new index |
| | | */ |
| | | Index newIndexForAttribute(TreeName indexName, Indexer indexer, int indexEntryLimit) |
| | | { |
| | | final int cursorEntryLimit = 100000; |
| | | return new Index(indexName, indexer, state, indexEntryLimit, cursorEntryLimit, false, storage, this); |
| | | } |
| | | |
| | | |
| | | /** |
| | | * Checks if any modifications apply to this indexed attribute. |
| | | * @param index the indexed attributes. |
| | | * @param mods the modifications to check for. |
| | | * @return true if any apply, false otherwise. |
| | | */ |
| | | private boolean isAttributeModified(AttributeIndex index, |
| | | List<Modification> mods) |
| | | { |
| | | boolean attributeModified = false; |
| | | AttributeType indexAttributeType = index.getAttributeType(); |
| | | Iterable<AttributeType> subTypes = |
| | | DirectoryServer.getSchema().getSubTypes(indexAttributeType); |
| | | |
| | | for (Modification mod : mods) |
| | | { |
| | | Attribute modAttr = mod.getAttribute(); |
| | | AttributeType modAttrType = modAttr.getAttributeType(); |
| | | if (modAttrType.equals(indexAttributeType)) |
| | | { |
| | | attributeModified = true; |
| | | break; |
| | | } |
| | | for(AttributeType subType : subTypes) |
| | | { |
| | | if(modAttrType.equals(subType)) |
| | | { |
| | | attributeModified = true; |
| | | break; |
| | | } |
| | | } |
| | | } |
| | | return attributeModified; |
| | | } |
| | | |
| | | |
| | | /** |
| | | * Fetch the base Entry of the EntryContainer. |
| | | * @param baseDN the DN for the base entry |
| | | * @param searchScope the scope under which this is fetched. |
| | | * Scope is used for referral processing. |
| | | * @return the Entry matching the baseDN. |
| | | * @throws DirectoryException if the baseDN doesn't exist. |
| | | */ |
| | | private Entry fetchBaseEntry(DN baseDN, SearchScope searchScope) |
| | | throws DirectoryException |
| | | { |
| | | // Fetch the base entry. |
| | | Entry baseEntry = null; |
| | | try |
| | | { |
| | | baseEntry = getEntry(baseDN); |
| | | } |
| | | catch (Exception e) |
| | | { |
| | | logger.traceException(e); |
| | | } |
| | | |
| | | // The base entry must exist for a successful result. |
| | | if (baseEntry == null) |
| | | { |
| | | // Check for referral entries above the base entry. |
| | | dn2uri.targetEntryReferrals(baseDN, searchScope); |
| | | |
| | | LocalizableMessage message = ERR_JEB_SEARCH_NO_SUCH_OBJECT.get(baseDN); |
| | | DN matchedDN = getMatchedDN(baseDN); |
| | | throw new DirectoryException(ResultCode.NO_SUCH_OBJECT, |
| | | message, matchedDN, null); |
| | | } |
| | | |
| | | return baseEntry; |
| | | } |
| | | |
| | | |
| | | /** |
| | | * Transform a database prefix string to one usable by the DB. |
| | | * @param databasePrefix the database prefix |
| | | * @return a new string when non letter or digit characters |
| | | * have been replaced with underscore |
| | | */ |
| | | private TreeName preparePrefix(String databasePrefix) |
| | | { |
| | | StringBuilder builder = new StringBuilder(databasePrefix.length()); |
| | | for (int i = 0; i < databasePrefix.length(); i++) |
| | | { |
| | | char ch = databasePrefix.charAt(i); |
| | | if (Character.isLetterOrDigit(ch)) |
| | | { |
| | | builder.append(ch); |
| | | } |
| | | else |
| | | { |
| | | builder.append('_'); |
| | | } |
| | | } |
| | | return TreeName.of(builder.toString()); |
| | | } |
| | | |
| | | /** Get the exclusive lock. */ |
| | | public void lock() { |
| | | exclusiveLock.lock(); |
| | | } |
| | | |
| | | /** Unlock the exclusive lock. */ |
| | | public void unlock() { |
| | | exclusiveLock.unlock(); |
| | | } |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public String toString() { |
| | | return databasePrefix.toString(); |
| | | } |
| | | } |
| New file |
| | |
| | | /* |
| | | * CDDL HEADER START |
| | | * |
| | | * The contents of this file are subject to the terms of the |
| | | * Common Development and Distribution License, Version 1.0 only |
| | | * (the "License"). You may not use this file except in compliance |
| | | * with the License. |
| | | * |
| | | * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt |
| | | * or http://forgerock.org/license/CDDLv1.0.html. |
| | | * See the License for the specific language governing permissions |
| | | * and limitations under the License. |
| | | * |
| | | * When distributing Covered Code, include this CDDL HEADER in each |
| | | * file and include the License file at legal-notices/CDDLv1_0.txt. |
| | | * If applicable, add the following below this CDDL HEADER, with the |
| | | * fields enclosed by brackets "[]" replaced with your own identifying |
| | | * information: |
| | | * Portions Copyright [yyyy] [name of copyright owner] |
| | | * |
| | | * CDDL HEADER END |
| | | * |
| | | * |
| | | * Copyright 2006-2008 Sun Microsystems, Inc. |
| | | * Portions Copyright 2014 ForgeRock AS |
| | | */ |
| | | package org.opends.server.backends.pluggable; |
| | | |
| | | import org.forgerock.opendj.ldap.ByteString; |
| | | |
| | | /** |
| | | * An integer identifier assigned to each entry in the JE backend. |
| | | * An entry ID is implemented by this class as a long. |
| | | * There are static methods to assign monotonically increasing entry IDs, |
| | | * starting from 1. |
| | | */ |
| | | public class EntryID implements Comparable<EntryID> |
| | | { |
| | | /** The identifier integer value. */ |
| | | private final long id; |
| | | /** The value in database format, created when necessary. */ |
| | | private ByteString value; |
| | | |
| | | /** |
| | | * Create a new entry ID object from a given long value. |
| | | * @param id The long value of the ID. |
| | | */ |
| | | public EntryID(long id) |
| | | { |
| | | this.id = id; |
| | | } |
| | | |
| | | /** |
| | | * Create a new entry ID object from a value in database format. |
| | | * @param value The database value of the ID. |
| | | */ |
| | | public EntryID(ByteString value) |
| | | { |
| | | this.value = value; |
| | | id = value.toLong(); |
| | | } |
| | | |
| | | /** |
| | | * Get the value of the entry ID as a long. |
| | | * @return The entry ID. |
| | | */ |
| | | public long longValue() |
| | | { |
| | | return id; |
| | | } |
| | | |
| | | /** |
| | | * Get the value of the ID in database format. |
| | | * @return The value of the ID in database format. |
| | | */ |
| | | public ByteString toByteString() |
| | | { |
| | | if (value == null) |
| | | { |
| | | value = ByteString.valueOf(id); |
| | | } |
| | | return value; |
| | | } |
| | | |
| | | /** |
| | | * Compares this object with the specified object for order. Returns a |
| | | * negative integer, zero, or a positive integer as this object is less |
| | | * than, equal to, or greater than the specified object.<p> |
| | | * <p/> |
| | | * |
| | | * @param that the Object to be compared. |
| | | * @return a negative integer, zero, or a positive integer as this object |
| | | * is less than, equal to, or greater than the specified object. |
| | | * @throws ClassCastException if the specified object's type prevents it |
| | | * from being compared to this Object. |
| | | */ |
| | | @Override |
| | | public int compareTo(EntryID that) throws ClassCastException |
| | | { |
| | | final long result = this.id - that.id; |
| | | if (result < 0) |
| | | { |
| | | return -1; |
| | | } |
| | | else if (result > 0) |
| | | { |
| | | return 1; |
| | | } |
| | | return 0; |
| | | } |
| | | |
| | | /** |
| | | * Indicates whether some other object is "equal to" this one. |
| | | * |
| | | * @param that the reference object with which to compare. |
| | | * @return <code>true</code> if this object is the same as the obj |
| | | * argument; <code>false</code> otherwise. |
| | | * @see #hashCode() |
| | | * @see java.util.Hashtable |
| | | */ |
| | | @Override |
| | | public boolean equals(Object that) |
| | | { |
| | | if (this == that) |
| | | { |
| | | return true; |
| | | } |
| | | if (!(that instanceof EntryID)) |
| | | { |
| | | return false; |
| | | } |
| | | return this.id == ((EntryID) that).id; |
| | | } |
| | | |
| | | /** |
| | | * Returns a hash code value for the object. This method is |
| | | * supported for the benefit of hashtables such as those provided by |
| | | * <code>java.util.Hashtable</code>. |
| | | * |
| | | * @return a hash code value for this object. |
| | | * @see java.lang.Object#equals(java.lang.Object) |
| | | * @see java.util.Hashtable |
| | | */ |
| | | @Override |
| | | public int hashCode() |
| | | { |
| | | return (int) id; |
| | | } |
| | | |
| | | /** |
| | | * Get a string representation of this object. |
| | | * @return A string representation of this object. |
| | | */ |
| | | @Override |
| | | public String toString() |
| | | { |
| | | return Long.toString(id); |
| | | } |
| | | } |
| New file |
| | |
| | | /* |
| | | * CDDL HEADER START |
| | | * |
| | | * The contents of this file are subject to the terms of the |
| | | * Common Development and Distribution License, Version 1.0 only |
| | | * (the "License"). You may not use this file except in compliance |
| | | * with the License. |
| | | * |
| | | * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt |
| | | * or http://forgerock.org/license/CDDLv1.0.html. |
| | | * See the License for the specific language governing permissions |
| | | * and limitations under the License. |
| | | * |
| | | * When distributing Covered Code, include this CDDL HEADER in each |
| | | * file and include the License file at legal-notices/CDDLv1_0.txt. |
| | | * If applicable, add the following below this CDDL HEADER, with the |
| | | * fields enclosed by brackets "[]" replaced with your own identifying |
| | | * information: |
| | | * Portions Copyright [yyyy] [name of copyright owner] |
| | | * |
| | | * CDDL HEADER END |
| | | * |
| | | * |
| | | * Copyright 2006-2008 Sun Microsystems, Inc. |
| | | * Portions Copyright 2014 ForgeRock AS |
| | | */ |
| | | package org.opends.server.backends.pluggable; |
| | | |
| | | import java.util.ArrayList; |
| | | import java.util.Arrays; |
| | | import java.util.Iterator; |
| | | |
| | | import org.forgerock.opendj.ldap.ByteSequence; |
| | | import org.forgerock.opendj.ldap.ByteString; |
| | | |
| | | /** |
| | | * Represents a set of Entry IDs. It can represent a set where the IDs are |
| | | * not defined, for example when the index entry limit has been exceeded. |
| | | */ |
| | | public class EntryIDSet implements Iterable<EntryID> |
| | | { |
| | | |
| | | /** |
| | | * The IDs are stored here in an array in ascending order. |
| | | * A null array implies not defined, rather than zero IDs. |
| | | */ |
| | | private long[] values; |
| | | |
| | | /** |
| | | * The size of the set when it is not defined. This value is only maintained |
| | | * when the set is undefined. |
| | | */ |
| | | private long undefinedSize = Long.MAX_VALUE; |
| | | |
| | | /** |
| | | * The database key containing this set, if the set was constructed |
| | | * directly from the database. |
| | | */ |
| | | private final ByteSequence key; |
| | | |
| | | /** Create a new undefined set. */ |
| | | public EntryIDSet() |
| | | { |
| | | this.key = null; |
| | | this.undefinedSize = Long.MAX_VALUE; |
| | | } |
| | | |
| | | /** |
| | | * Create a new undefined set with a initial size. |
| | | * |
| | | * @param size The undefined size for this set. |
| | | */ |
| | | public EntryIDSet(long size) |
| | | { |
| | | this.key = null; |
| | | this.undefinedSize = size; |
| | | } |
| | | |
| | | /** |
| | | * Create a new entry ID set from the raw database value. |
| | | * |
| | | * @param keyBytes The database key that contains this value. |
| | | * @param bytes The database value, or null if there are no entry IDs. |
| | | */ |
| | | public EntryIDSet(byte[] keyBytes, byte[] bytes) |
| | | { |
| | | this(keyBytes != null ? ByteString.wrap(keyBytes) : null, |
| | | bytes != null ? ByteString.wrap(bytes) : null); |
| | | } |
| | | |
| | | /** |
| | | * Create a new entry ID set from the raw database value. |
| | | * |
| | | * @param key |
| | | * The database key that contains this value. |
| | | * @param bytes |
| | | * The database value, or null if there are no entry IDs. |
| | | */ |
| | | public EntryIDSet(ByteSequence key, ByteString bytes) |
| | | { |
| | | this.key = key; |
| | | |
| | | if (bytes == null) |
| | | { |
| | | values = new long[0]; |
| | | return; |
| | | } |
| | | |
| | | if (bytes.length() == 0) |
| | | { |
| | | // Entry limit has exceeded and there is no encoded undefined set size. |
| | | undefinedSize = Long.MAX_VALUE; |
| | | } |
| | | else if ((bytes.byteAt(0) & 0x80) == 0x80) |
| | | { |
| | | // Entry limit has exceeded and there is an encoded undefined set size. |
| | | undefinedSize = |
| | | JebFormat.entryIDUndefinedSizeFromDatabase(bytes.toByteArray()); |
| | | } |
| | | else |
| | | { |
| | | // Seems like entry limit has not been exceeded and the bytes is a |
| | | // list of entry IDs. |
| | | values = JebFormat.entryIDListFromDatabase(bytes); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Construct an EntryIDSet from an array of longs. |
| | | * |
| | | * @param values The array of IDs represented as longs. |
| | | * @param pos The position of the first ID to take from the array. |
| | | * @param len the number of IDs to take from the array. |
| | | */ |
| | | EntryIDSet(long[] values, int pos, int len) |
| | | { |
| | | this.key = null; |
| | | this.values = new long[len]; |
| | | System.arraycopy(values, pos, this.values, 0, len); |
| | | } |
| | | |
| | | /** |
| | | * Create a new set of entry IDs that is the union of several entry ID sets. |
| | | * |
| | | * @param sets A list of entry ID sets. |
| | | * @param allowDuplicates true if duplicate IDs are allowed in the resulting |
| | | * set, or if the provided sets are sure not to overlap; false if |
| | | * duplicates should be eliminated. |
| | | * @return The union of the provided entry ID sets. |
| | | */ |
| | | public static EntryIDSet unionOfSets(ArrayList<EntryIDSet> sets, |
| | | boolean allowDuplicates) |
| | | { |
| | | int count = 0; |
| | | |
| | | boolean undefined = false; |
| | | for (EntryIDSet l : sets) |
| | | { |
| | | if (!l.isDefined()) |
| | | { |
| | | if(l.undefinedSize == Long.MAX_VALUE) |
| | | { |
| | | return new EntryIDSet(); |
| | | } |
| | | undefined = true; |
| | | } |
| | | count += l.size(); |
| | | } |
| | | |
| | | if(undefined) |
| | | { |
| | | return new EntryIDSet(count); |
| | | } |
| | | |
| | | boolean needSort = false; |
| | | long[] n = new long[count]; |
| | | int pos = 0; |
| | | for (EntryIDSet l : sets) |
| | | { |
| | | if (l.values.length != 0) |
| | | { |
| | | if (!needSort && pos > 0 && l.values[0] < n[pos-1]) |
| | | { |
| | | needSort = true; |
| | | } |
| | | System.arraycopy(l.values, 0, n, pos, l.values.length); |
| | | pos += l.values.length; |
| | | } |
| | | } |
| | | if (needSort) |
| | | { |
| | | Arrays.sort(n); |
| | | } |
| | | if (allowDuplicates) |
| | | { |
| | | EntryIDSet ret = new EntryIDSet(); |
| | | ret.values = n; |
| | | return ret; |
| | | } |
| | | long[] n1 = new long[n.length]; |
| | | long last = -1; |
| | | int j = 0; |
| | | for (long l : n) |
| | | { |
| | | if (l != last) |
| | | { |
| | | last = n1[j++] = l; |
| | | } |
| | | } |
| | | if (j == n1.length) |
| | | { |
| | | EntryIDSet ret = new EntryIDSet(); |
| | | ret.values = n1; |
| | | return ret; |
| | | } |
| | | else |
| | | { |
| | | return new EntryIDSet(n1, 0, j); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Get the size of this entry ID set. |
| | | * |
| | | * @return The number of IDs in the set. |
| | | */ |
| | | public long size() |
| | | { |
| | | if (values != null) |
| | | { |
| | | return values.length; |
| | | } |
| | | return undefinedSize; |
| | | } |
| | | |
| | | /** |
| | | * Get a string representation of this object. |
| | | * @return A string representation of this object. |
| | | */ |
| | | @Override |
| | | public String toString() |
| | | { |
| | | StringBuilder buffer = new StringBuilder(16); |
| | | toString(buffer); |
| | | return buffer.toString(); |
| | | } |
| | | |
| | | /** |
| | | * Convert to a short string to aid with debugging. |
| | | * |
| | | * @param buffer The string is appended to this string builder. |
| | | */ |
| | | public void toString(StringBuilder buffer) |
| | | { |
| | | if (!isDefined()) |
| | | { |
| | | if (key != null) |
| | | { |
| | | // The index entry limit was exceeded |
| | | if(undefinedSize == Long.MAX_VALUE) |
| | | { |
| | | buffer.append("[LIMIT-EXCEEDED]"); |
| | | } |
| | | else |
| | | { |
| | | buffer.append("[LIMIT-EXCEEDED:"); |
| | | buffer.append(undefinedSize); |
| | | buffer.append("]"); |
| | | } |
| | | } |
| | | else |
| | | { |
| | | // Not indexed |
| | | buffer.append("[NOT-INDEXED]"); |
| | | } |
| | | } |
| | | else |
| | | { |
| | | buffer.append("[COUNT:"); |
| | | buffer.append(size()); |
| | | buffer.append("]"); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Determine whether this set of IDs is defined. |
| | | * |
| | | * @return true if the set of IDs is defined. |
| | | */ |
| | | public boolean isDefined() |
| | | { |
| | | return values != null; |
| | | } |
| | | |
| | | /** |
| | | * Get a database representation of this object. |
| | | * @return A database representation of this object as a byte array. |
| | | */ |
| | | public ByteString toByteString() |
| | | { |
| | | if(isDefined()) |
| | | { |
| | | return ByteString.wrap(JebFormat.entryIDListToDatabase(values)); |
| | | } |
| | | else |
| | | { |
| | | return ByteString.wrap(JebFormat.entryIDUndefinedSizeToDatabase(undefinedSize)); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Insert an ID into this set. |
| | | * |
| | | * @param entryID The ID to be inserted. |
| | | * @return true if the set was changed, false if it was not changed, |
| | | * for example if the set is undefined or the ID was already present. |
| | | */ |
| | | public boolean add(EntryID entryID) |
| | | { |
| | | if (values == null) |
| | | { |
| | | if(undefinedSize != Long.MAX_VALUE) |
| | | { |
| | | undefinedSize++; |
| | | } |
| | | return true; |
| | | } |
| | | |
| | | long id = entryID.longValue(); |
| | | if (values.length == 0) |
| | | { |
| | | values = new long[] { id }; |
| | | return true; |
| | | } |
| | | |
| | | if (id > values[values.length-1]) |
| | | { |
| | | long[] updatedValues = Arrays.copyOf(values, values.length + 1); |
| | | updatedValues[values.length] = id; |
| | | values = updatedValues; |
| | | } |
| | | else |
| | | { |
| | | int pos = Arrays.binarySearch(values, id); |
| | | if (pos >= 0) |
| | | { |
| | | // The ID is already present. |
| | | return false; |
| | | } |
| | | |
| | | // For a negative return value r, the index -(r+1) gives the array |
| | | // index at which the specified value can be inserted to maintain |
| | | // the sorted order of the array. |
| | | pos = -(pos+1); |
| | | |
| | | long[] updatedValues = new long[values.length+1]; |
| | | System.arraycopy(values, 0, updatedValues, 0, pos); |
| | | System.arraycopy(values, pos, updatedValues, pos+1, values.length-pos); |
| | | updatedValues[pos] = id; |
| | | values = updatedValues; |
| | | } |
| | | |
| | | return true; |
| | | } |
| | | |
| | | /** |
| | | * Remove an ID from this set. |
| | | * |
| | | * @param entryID The ID to be removed |
| | | * @return true if the set was changed, false if it was not changed, |
| | | * for example if the set was undefined or the ID was not present. |
| | | */ |
| | | public boolean remove(EntryID entryID) |
| | | { |
| | | if (values == null) |
| | | { |
| | | if(undefinedSize != Long.MAX_VALUE) |
| | | { |
| | | undefinedSize--; |
| | | } |
| | | return true; |
| | | } |
| | | |
| | | if (values.length == 0) |
| | | { |
| | | return false; |
| | | } |
| | | |
| | | // Binary search to locate the ID. |
| | | long id = entryID.longValue(); |
| | | int pos = Arrays.binarySearch(values, id); |
| | | if (pos >= 0) |
| | | { |
| | | // Found it. |
| | | long[] updatedValues = new long[values.length-1]; |
| | | System.arraycopy(values, 0, updatedValues, 0, pos); |
| | | System.arraycopy(values, pos+1, updatedValues, pos, values.length-pos-1); |
| | | values = updatedValues; |
| | | return true; |
| | | } |
| | | // Not found. |
| | | return false; |
| | | } |
| | | |
| | | /** |
| | | * Check whether this set of entry IDs contains a given ID. |
| | | * |
| | | * @param entryID The ID to be checked. |
| | | * @return true if this set contains the given ID, |
| | | * or if the set is undefined. |
| | | */ |
| | | public boolean contains(EntryID entryID) |
| | | { |
| | | if (values == null) |
| | | { |
| | | return true; |
| | | } |
| | | |
| | | final long id = entryID.longValue(); |
| | | return values.length != 0 |
| | | && id <= values[values.length - 1] |
| | | && Arrays.binarySearch(values, id) >= 0; |
| | | } |
| | | |
| | | /** |
| | | * Takes the intersection of this set with another. |
| | | * Retain those IDs that appear in the given set. |
| | | * |
| | | * @param that The set of IDs that are to be retained from this object. |
| | | */ |
| | | public void retainAll(EntryIDSet that) |
| | | { |
| | | if (!isDefined()) |
| | | { |
| | | this.values = that.values; |
| | | this.undefinedSize = that.undefinedSize; |
| | | return; |
| | | } |
| | | |
| | | if (!that.isDefined()) |
| | | { |
| | | return; |
| | | } |
| | | |
| | | // TODO Perhaps Arrays.asList and retainAll list method are more efficient? |
| | | |
| | | long[] a = this.values; |
| | | long[] b = that.values; |
| | | |
| | | int ai = 0, bi = 0, ci = 0; |
| | | long[] c = new long[Math.min(a.length,b.length)]; |
| | | while (ai < a.length && bi < b.length) |
| | | { |
| | | if (a[ai] == b[bi]) |
| | | { |
| | | c[ci] = a[ai]; |
| | | ai++; |
| | | bi++; |
| | | ci++; |
| | | } |
| | | else if (a[ai] > b[bi]) |
| | | { |
| | | bi++; |
| | | } |
| | | else |
| | | { |
| | | ai++; |
| | | } |
| | | } |
| | | if (ci < c.length) |
| | | { |
| | | values = Arrays.copyOf(c, ci); |
| | | } |
| | | else |
| | | { |
| | | values = c; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Add all the IDs from a given set that are not already present. |
| | | * |
| | | * @param that The set of IDs to be added. It MUST be defined |
| | | */ |
| | | public void addAll(EntryIDSet that) |
| | | { |
| | | if(!that.isDefined()) |
| | | { |
| | | return; |
| | | } |
| | | |
| | | if (!isDefined()) |
| | | { |
| | | // Assume there are no overlap between IDs in that set with this set |
| | | if(undefinedSize != Long.MAX_VALUE) |
| | | { |
| | | undefinedSize += that.size(); |
| | | } |
| | | return; |
| | | } |
| | | |
| | | long[] a = this.values; |
| | | long[] b = that.values; |
| | | |
| | | if (a.length == 0) |
| | | { |
| | | values = b; |
| | | return; |
| | | } |
| | | |
| | | if (b.length == 0) |
| | | { |
| | | return; |
| | | } |
| | | |
| | | // Optimize for case where the two sets are sure to have no overlap. |
| | | if (b[0] > a[a.length-1]) |
| | | { |
| | | // All IDs in 'b' are greater than those in 'a'. |
| | | long[] n = new long[a.length + b.length]; |
| | | System.arraycopy(a, 0, n, 0, a.length); |
| | | System.arraycopy(b, 0, n, a.length, b.length); |
| | | values = n; |
| | | return; |
| | | } |
| | | |
| | | if (a[0] > b[b.length-1]) |
| | | { |
| | | // All IDs in 'a' are greater than those in 'b'. |
| | | long[] n = new long[a.length + b.length]; |
| | | System.arraycopy(b, 0, n, 0, b.length); |
| | | System.arraycopy(a, 0, n, b.length, a.length); |
| | | values = n; |
| | | return; |
| | | } |
| | | |
| | | long[] n; |
| | | if ( b.length < a.length ) { |
| | | n = a; |
| | | a = b; |
| | | b = n; |
| | | } |
| | | |
| | | n = new long[a.length + b.length]; |
| | | |
| | | int ai, bi, ni; |
| | | for ( ni = 0, ai = 0, bi = 0; ai < a.length && bi < b.length; ) { |
| | | if ( a[ai] < b[bi] ) { |
| | | n[ni++] = a[ai++]; |
| | | } else if ( b[bi] < a[ai] ) { |
| | | n[ni++] = b[bi++]; |
| | | } else { |
| | | n[ni++] = a[ai]; |
| | | ai++; |
| | | bi++; |
| | | } |
| | | } |
| | | |
| | | // Copy any remainder from the first array. |
| | | int aRemain = a.length - ai; |
| | | if (aRemain > 0) |
| | | { |
| | | System.arraycopy(a, ai, n, ni, aRemain); |
| | | ni += aRemain; |
| | | } |
| | | |
| | | // Copy any remainder from the second array. |
| | | int bRemain = b.length - bi; |
| | | if (bRemain > 0) |
| | | { |
| | | System.arraycopy(b, bi, n, ni, bRemain); |
| | | ni += bRemain; |
| | | } |
| | | |
| | | if (ni < n.length) |
| | | { |
| | | values = Arrays.copyOf(n, ni); |
| | | } |
| | | else |
| | | { |
| | | values = n; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Delete all IDs in this set that are in a given set. |
| | | * |
| | | * @param that The set of IDs to be deleted. It MUST be defined. |
| | | */ |
| | | public void deleteAll(EntryIDSet that) |
| | | { |
| | | if(!that.isDefined()) |
| | | { |
| | | return; |
| | | } |
| | | |
| | | if (!isDefined()) |
| | | { |
| | | // Assume all IDs in the given set exists in this set. |
| | | if(undefinedSize != Long.MAX_VALUE) |
| | | { |
| | | undefinedSize -= that.size(); |
| | | } |
| | | return; |
| | | } |
| | | |
| | | long[] a = this.values; |
| | | long[] b = that.values; |
| | | |
| | | if (a.length == 0 || b.length == 0 |
| | | // Optimize for cases where the two sets are sure to have no overlap. |
| | | || b[0] > a[a.length-1] |
| | | || a[0] > b[b.length-1]) |
| | | { |
| | | return; |
| | | } |
| | | |
| | | long[] n = new long[a.length]; |
| | | |
| | | int ai, bi, ni; |
| | | for ( ni = 0, ai = 0, bi = 0; ai < a.length && bi < b.length; ) { |
| | | if ( a[ai] < b[bi] ) { |
| | | n[ni++] = a[ai++]; |
| | | } else if ( b[bi] < a[ai] ) { |
| | | bi++; |
| | | } else { |
| | | ai++; |
| | | bi++; |
| | | } |
| | | } |
| | | |
| | | System.arraycopy(a, ai, n, ni, a.length - ai); |
| | | ni += a.length - ai; |
| | | |
| | | if (ni < a.length) |
| | | { |
| | | values = Arrays.copyOf(n, ni); |
| | | } |
| | | else |
| | | { |
| | | values = n; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Create an iterator over the set or an empty iterator |
| | | * if the set is not defined. |
| | | * |
| | | * @return An EntryID iterator. |
| | | */ |
| | | @Override |
| | | public Iterator<EntryID> iterator() |
| | | { |
| | | return iterator(null); |
| | | } |
| | | |
| | | /** |
| | | * Create an iterator over the set or an empty iterator |
| | | * if the set is not defined. |
| | | * |
| | | * @param begin The entry ID of the first entry to return in the list. |
| | | * |
| | | * @return An EntryID iterator. |
| | | */ |
| | | public Iterator<EntryID> iterator(EntryID begin) |
| | | { |
| | | if (values != null) |
| | | { |
| | | // The set is defined. |
| | | return new IDSetIterator(values, begin); |
| | | } |
| | | // The set is not defined. |
| | | return new IDSetIterator(new long[0]); |
| | | } |
| | | |
| | | } |
| New file |
| | |
| | | /* |
| | | * CDDL HEADER START |
| | | * |
| | | * The contents of this file are subject to the terms of the |
| | | * Common Development and Distribution License, Version 1.0 only |
| | | * (the "License"). You may not use this file except in compliance |
| | | * with the License. |
| | | * |
| | | * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt |
| | | * or http://forgerock.org/license/CDDLv1.0.html. |
| | | * See the License for the specific language governing permissions |
| | | * and limitations under the License. |
| | | * |
| | | * When distributing Covered Code, include this CDDL HEADER in each |
| | | * file and include the License file at legal-notices/CDDLv1_0.txt. |
| | | * If applicable, add the following below this CDDL HEADER, with the |
| | | * fields enclosed by brackets "[]" replaced with your own identifying |
| | | * information: |
| | | * Portions Copyright [yyyy] [name of copyright owner] |
| | | * |
| | | * CDDL HEADER END |
| | | * |
| | | * |
| | | * Copyright 2008 Sun Microsystems, Inc. |
| | | * Portions Copyright 2011-2014 ForgeRock AS |
| | | */ |
| | | package org.opends.server.backends.pluggable; |
| | | |
| | | import java.util.Iterator; |
| | | import java.util.LinkedList; |
| | | import java.util.Map; |
| | | import java.util.TreeMap; |
| | | |
| | | import org.forgerock.i18n.LocalizableMessage; |
| | | import org.forgerock.opendj.ldap.ByteString; |
| | | import org.forgerock.opendj.ldap.ResultCode; |
| | | import org.forgerock.opendj.ldap.SearchScope; |
| | | import org.opends.server.backends.pluggable.SuffixContainer; |
| | | import org.opends.server.controls.VLVRequestControl; |
| | | import org.opends.server.controls.VLVResponseControl; |
| | | import org.opends.server.core.DirectoryServer; |
| | | import org.opends.server.core.SearchOperation; |
| | | import org.opends.server.protocols.ldap.LDAPResultCode; |
| | | import org.opends.server.types.*; |
| | | |
| | | import static org.opends.messages.JebMessages.*; |
| | | import static org.opends.server.util.StaticUtils.*; |
| | | |
| | | /** |
| | | * This class provides a mechanism for sorting the contents of an entry ID set |
| | | * based on a given sort order. |
| | | */ |
| | | public class EntryIDSetSorter |
| | | { |
| | | /** |
| | | * Creates a new entry ID set which is a sorted representation of the provided |
| | | * set using the given sort order. |
| | | * |
| | | * @param suffixContainer The suffix container with which the ID list is associated. |
| | | * @param entryIDSet The entry ID set to be sorted. |
| | | * @param searchOperation The search operation being processed. |
| | | * @param sortOrder The sort order to use for the entry ID set. |
| | | * @param vlvRequest The VLV request control included in the search |
| | | * request, or {@code null} if there was none. |
| | | * |
| | | * @return A new entry ID set which is a sorted representation of the |
| | | * provided set using the given sort order. |
| | | * |
| | | * @throws DirectoryException If an error occurs while performing the sort. |
| | | */ |
| | | public static EntryIDSet sort(SuffixContainer suffixContainer, |
| | | EntryIDSet entryIDSet, |
| | | SearchOperation searchOperation, |
| | | SortOrder sortOrder, |
| | | VLVRequestControl vlvRequest) |
| | | throws DirectoryException |
| | | { |
| | | if (! entryIDSet.isDefined()) |
| | | { |
| | | return new EntryIDSet(); |
| | | } |
| | | |
| | | DN baseDN = searchOperation.getBaseDN(); |
| | | SearchScope scope = searchOperation.getScope(); |
| | | SearchFilter filter = searchOperation.getFilter(); |
| | | |
| | | TreeMap<SortValues,EntryID> sortMap = new TreeMap<SortValues,EntryID>(); |
| | | for (EntryID id : entryIDSet) |
| | | { |
| | | try |
| | | { |
| | | Entry e = suffixContainer.getEntry(id); |
| | | if (e.matchesBaseAndScope(baseDN, scope) && filter.matchesEntry(e)) |
| | | { |
| | | sortMap.put(new SortValues(id, e, sortOrder), id); |
| | | } |
| | | } |
| | | catch (Exception e) |
| | | { |
| | | LocalizableMessage message = ERR_ENTRYIDSORTER_CANNOT_EXAMINE_ENTRY.get(id, getExceptionMessage(e)); |
| | | throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message, e); |
| | | } |
| | | } |
| | | |
| | | |
| | | // See if there is a VLV request to further pare down the set of results, |
| | | // and if there is where it should be processed by offset or assertion value. |
| | | long[] sortedIDs; |
| | | if (vlvRequest != null) |
| | | { |
| | | int beforeCount = vlvRequest.getBeforeCount(); |
| | | int afterCount = vlvRequest.getAfterCount(); |
| | | |
| | | if (vlvRequest.getTargetType() == VLVRequestControl.TYPE_TARGET_BYOFFSET) |
| | | { |
| | | int targetOffset = vlvRequest.getOffset(); |
| | | if (targetOffset < 0) |
| | | { |
| | | // The client specified a negative target offset. This should never be allowed. |
| | | searchOperation.addResponseControl( |
| | | new VLVResponseControl(targetOffset, sortMap.size(), |
| | | LDAPResultCode.OFFSET_RANGE_ERROR)); |
| | | |
| | | LocalizableMessage message = ERR_ENTRYIDSORTER_NEGATIVE_START_POS.get(); |
| | | throw new DirectoryException(ResultCode.VIRTUAL_LIST_VIEW_ERROR, |
| | | message); |
| | | } |
| | | else if (targetOffset == 0) |
| | | { |
| | | // This is an easy mistake to make, since VLV offsets start at 1 |
| | | // instead of 0. We'll assume the client meant to use 1. |
| | | targetOffset = 1; |
| | | } |
| | | |
| | | int listOffset = targetOffset - 1; // VLV offsets start at 1, not 0. |
| | | int startPos = listOffset - beforeCount; |
| | | if (startPos < 0) |
| | | { |
| | | // This can happen if beforeCount >= offset, and in this case we'll |
| | | // just adjust the start position to ignore the range of beforeCount |
| | | // that doesn't exist. |
| | | startPos = 0; |
| | | beforeCount = listOffset; |
| | | } |
| | | else if (startPos >= sortMap.size()) |
| | | { |
| | | // The start position is beyond the end of the list. In this case, |
| | | // we'll assume that the start position was one greater than the |
| | | // size of the list and will only return the beforeCount entries. |
| | | targetOffset = sortMap.size() + 1; |
| | | listOffset = sortMap.size(); |
| | | startPos = listOffset - beforeCount; |
| | | afterCount = 0; |
| | | } |
| | | |
| | | int count = 1 + beforeCount + afterCount; |
| | | sortedIDs = new long[count]; |
| | | |
| | | int treePos = 0; |
| | | int arrayPos = 0; |
| | | for (EntryID id : sortMap.values()) |
| | | { |
| | | if (treePos++ < startPos) |
| | | { |
| | | continue; |
| | | } |
| | | |
| | | sortedIDs[arrayPos++] = id.longValue(); |
| | | if (arrayPos >= count) |
| | | { |
| | | break; |
| | | } |
| | | } |
| | | |
| | | if (arrayPos < count) |
| | | { |
| | | // We don't have enough entries in the set to meet the requested |
| | | // page size, so we'll need to shorten the array. |
| | | long[] newIDArray = new long[arrayPos]; |
| | | System.arraycopy(sortedIDs, 0, newIDArray, 0, arrayPos); |
| | | sortedIDs = newIDArray; |
| | | } |
| | | |
| | | searchOperation.addResponseControl( |
| | | new VLVResponseControl(targetOffset, sortMap.size(), |
| | | LDAPResultCode.SUCCESS)); |
| | | } |
| | | else |
| | | { |
| | | ByteString assertionValue = vlvRequest.getGreaterThanOrEqualAssertion(); |
| | | |
| | | boolean targetFound = false; |
| | | int targetOffset = 0; |
| | | int includedBeforeCount = 0; |
| | | int includedAfterCount = 0; |
| | | int listSize = 0; |
| | | LinkedList<EntryID> idList = new LinkedList<EntryID>(); |
| | | for (Map.Entry<SortValues, EntryID> entry : sortMap.entrySet()) |
| | | { |
| | | SortValues sortValues = entry.getKey(); |
| | | EntryID id = entry.getValue(); |
| | | |
| | | if (targetFound) |
| | | { |
| | | idList.add(id); |
| | | listSize++; |
| | | includedAfterCount++; |
| | | if (includedAfterCount >= afterCount) |
| | | { |
| | | break; |
| | | } |
| | | } |
| | | else |
| | | { |
| | | targetFound = sortValues.compareTo(assertionValue) >= 0; |
| | | targetOffset++; |
| | | |
| | | if (targetFound) |
| | | { |
| | | idList.add(id); |
| | | listSize++; |
| | | } |
| | | else if (beforeCount > 0) |
| | | { |
| | | idList.add(id); |
| | | includedBeforeCount++; |
| | | if (includedBeforeCount > beforeCount) |
| | | { |
| | | idList.removeFirst(); |
| | | includedBeforeCount--; |
| | | } |
| | | else |
| | | { |
| | | listSize++; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | if (! targetFound) |
| | | { |
| | | // No entry was found to be greater than or equal to the sort key, so |
| | | // the target offset will be one greater than the content count. |
| | | targetOffset = sortMap.size() + 1; |
| | | } |
| | | |
| | | sortedIDs = new long[listSize]; |
| | | Iterator<EntryID> idIterator = idList.iterator(); |
| | | for (int i=0; i < listSize; i++) |
| | | { |
| | | sortedIDs[i] = idIterator.next().longValue(); |
| | | } |
| | | |
| | | searchOperation.addResponseControl( |
| | | new VLVResponseControl(targetOffset, sortMap.size(), |
| | | LDAPResultCode.SUCCESS)); |
| | | } |
| | | } |
| | | else |
| | | { |
| | | sortedIDs = new long[sortMap.size()]; |
| | | int i=0; |
| | | for (EntryID id : sortMap.values()) |
| | | { |
| | | sortedIDs[i++] = id.longValue(); |
| | | } |
| | | } |
| | | |
| | | return new EntryIDSet(sortedIDs, 0, sortedIDs.length); |
| | | } |
| | | } |
| New file |
| | |
| | | /* |
| | | * CDDL HEADER START |
| | | * |
| | | * The contents of this file are subject to the terms of the |
| | | * Common Development and Distribution License, Version 1.0 only |
| | | * (the "License"). You may not use this file except in compliance |
| | | * with the License. |
| | | * |
| | | * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt |
| | | * or http://forgerock.org/license/CDDLv1.0.html. |
| | | * See the License for the specific language governing permissions |
| | | * and limitations under the License. |
| | | * |
| | | * When distributing Covered Code, include this CDDL HEADER in each |
| | | * file and include the License file at legal-notices/CDDLv1_0.txt. |
| | | * If applicable, add the following below this CDDL HEADER, with the |
| | | * fields enclosed by brackets "[]" replaced with your own identifying |
| | | * information: |
| | | * Portions Copyright [yyyy] [name of copyright owner] |
| | | * |
| | | * CDDL HEADER END |
| | | * |
| | | * |
| | | * Copyright 2006-2009 Sun Microsystems, Inc. |
| | | * Portions Copyright 2014 ForgeRock AS |
| | | */ |
| | | package org.opends.server.backends.pluggable; |
| | | import org.forgerock.i18n.LocalizableMessage; |
| | | |
| | | import org.forgerock.i18n.slf4j.LocalizedLogger; |
| | | import static org.opends.messages.JebMessages.*; |
| | | |
| | | import java.io.File; |
| | | import java.io.FilenameFilter; |
| | | |
| | | /** |
| | | * A singleton class to manage the life-cycle of a JE database environment. |
| | | */ |
| | | public class EnvManager |
| | | { |
| | | private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); |
| | | |
| | | |
| | | /** |
| | | * A filename filter to match all kinds of JE files. |
| | | */ |
| | | private static final FilenameFilter jeAllFilesFilter; |
| | | |
| | | static |
| | | { |
| | | // A filename filter to match all kinds of JE files. |
| | | // JE has a com.sleepycat.je.log.JEFileFilter that would be useful |
| | | // here but is not public. |
| | | jeAllFilesFilter = new FilenameFilter() |
| | | { |
| | | public boolean accept(File d, String name) |
| | | { |
| | | return name.endsWith(".jdb") || |
| | | name.endsWith(".del") || |
| | | name.startsWith("je."); |
| | | } |
| | | }; |
| | | } |
| | | |
| | | /** |
| | | * Creates the environment home directory, deleting any existing data files |
| | | * if the directory already exists. |
| | | * The environment must not be open. |
| | | * |
| | | * @param homeDir The backend home directory. |
| | | * @throws JebException If an error occurs in the JE backend. |
| | | */ |
| | | public static void createHomeDir(String homeDir) |
| | | throws JebException |
| | | { |
| | | File dir = new File(homeDir); |
| | | |
| | | if (dir.exists()) |
| | | { |
| | | if (!dir.isDirectory()) |
| | | { |
| | | LocalizableMessage message = ERR_JEB_DIRECTORY_INVALID.get(homeDir); |
| | | throw new JebException(message); |
| | | } |
| | | removeFiles(homeDir); |
| | | } |
| | | else |
| | | { |
| | | try |
| | | { |
| | | dir.mkdir(); |
| | | } |
| | | catch (Exception e) |
| | | { |
| | | logger.traceException(e); |
| | | LocalizableMessage message = ERR_JEB_CREATE_FAIL.get(e.getMessage()); |
| | | throw new JebException(message, e); |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Deletes all the data files associated with the environment. |
| | | * The environment must not be open. |
| | | * |
| | | * @param homeDir The backend home directory |
| | | * @throws JebException If an error occurs in the JE backend or if the |
| | | * specified home directory does not exist. |
| | | */ |
| | | public static void removeFiles(String homeDir) |
| | | throws JebException |
| | | { |
| | | File dir = new File(homeDir); |
| | | if (!dir.exists()) |
| | | { |
| | | LocalizableMessage message = ERR_JEB_DIRECTORY_DOES_NOT_EXIST.get(homeDir); |
| | | throw new JebException(message); |
| | | } |
| | | if (!dir.isDirectory()) |
| | | { |
| | | LocalizableMessage message = ERR_JEB_DIRECTORY_INVALID.get(homeDir); |
| | | throw new JebException(message); |
| | | } |
| | | |
| | | try |
| | | { |
| | | File[] jdbFiles = dir.listFiles(jeAllFilesFilter); |
| | | for (File f : jdbFiles) |
| | | { |
| | | f.delete(); |
| | | } |
| | | } |
| | | catch (Exception e) |
| | | { |
| | | logger.traceException(e); |
| | | LocalizableMessage message = ERR_JEB_REMOVE_FAIL.get(e.getMessage()); |
| | | throw new JebException(message, e); |
| | | } |
| | | } |
| | | |
| | | } |
| New file |
| | |
| | | /* |
| | | * CDDL HEADER START |
| | | * |
| | | * The contents of this file are subject to the terms of the |
| | | * Common Development and Distribution License, Version 1.0 only |
| | | * (the "License"). You may not use this file except in compliance |
| | | * with the License. |
| | | * |
| | | * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt |
| | | * or http://forgerock.org/license/CDDLv1.0.html. |
| | | * See the License for the specific language governing permissions |
| | | * and limitations under the License. |
| | | * |
| | | * When distributing Covered Code, include this CDDL HEADER in each |
| | | * file and include the License file at legal-notices/CDDLv1_0.txt. |
| | | * If applicable, add the following below this CDDL HEADER, with the |
| | | * fields enclosed by brackets "[]" replaced with your own identifying |
| | | * information: |
| | | * Portions Copyright [yyyy] [name of copyright owner] |
| | | * |
| | | * CDDL HEADER END |
| | | * |
| | | * |
| | | * Copyright 2006-2010 Sun Microsystems, Inc. |
| | | * Portions Copyright 2014 ForgeRock AS |
| | | */ |
| | | package org.opends.server.backends.pluggable; |
| | | |
| | | import java.util.Collection; |
| | | |
| | | import org.forgerock.opendj.ldap.ByteSequence; |
| | | import org.forgerock.opendj.ldap.ByteString; |
| | | import org.forgerock.opendj.ldap.DecodeException; |
| | | import org.forgerock.opendj.ldap.schema.MatchingRule; |
| | | import org.forgerock.opendj.ldap.schema.Schema; |
| | | import org.forgerock.opendj.ldap.spi.Indexer; |
| | | import org.forgerock.opendj.ldap.spi.IndexingOptions; |
| | | import org.opends.server.types.AttributeType; |
| | | |
| | | /** |
| | | * An implementation of an Indexer for attribute equality. |
| | | */ |
| | | public class EqualityIndexer implements Indexer |
| | | { |
| | | |
| | | /** |
| | | * The attribute type equality matching rule which is also the |
| | | * comparator for the index keys generated by this class. |
| | | */ |
| | | private final MatchingRule equalityRule; |
| | | |
| | | /** |
| | | * Create a new attribute equality indexer for the given index configuration. |
| | | * @param attributeType The attribute type for which an indexer is |
| | | * required. |
| | | */ |
| | | public EqualityIndexer(AttributeType attributeType) |
| | | { |
| | | this.equalityRule = attributeType.getEqualityMatchingRule(); |
| | | } |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public String getIndexID() |
| | | { |
| | | return "equality"; |
| | | } |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public void createKeys(Schema schema, ByteSequence value, |
| | | IndexingOptions options, Collection<ByteString> keys) |
| | | throws DecodeException |
| | | { |
| | | keys.add(equalityRule.normalizeAttributeValue(value)); |
| | | } |
| | | |
| | | } |
| New file |
| | |
| | | /* |
| | | * CDDL HEADER START |
| | | * |
| | | * The contents of this file are subject to the terms of the |
| | | * Common Development and Distribution License, Version 1.0 only |
| | | * (the "License"). You may not use this file except in compliance |
| | | * with the License. |
| | | * |
| | | * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt |
| | | * or http://forgerock.org/license/CDDLv1.0.html. |
| | | * See the License for the specific language governing permissions |
| | | * and limitations under the License. |
| | | * |
| | | * When distributing Covered Code, include this CDDL HEADER in each |
| | | * file and include the License file at legal-notices/CDDLv1_0.txt. |
| | | * If applicable, add the following below this CDDL HEADER, with the |
| | | * fields enclosed by brackets "[]" replaced with your own identifying |
| | | * information: |
| | | * Portions Copyright [yyyy] [name of copyright owner] |
| | | * |
| | | * CDDL HEADER END |
| | | * |
| | | * |
| | | * Copyright 2006-2008 Sun Microsystems, Inc. |
| | | * Portions Copyright 2012-2014 ForgeRock AS. |
| | | */ |
| | | package org.opends.server.backends.pluggable; |
| | | |
| | | import java.io.IOException; |
| | | import java.util.ArrayList; |
| | | import java.util.List; |
| | | import java.util.Timer; |
| | | import java.util.TimerTask; |
| | | |
| | | import org.forgerock.i18n.slf4j.LocalizedLogger; |
| | | import org.forgerock.opendj.ldap.ByteString; |
| | | import org.opends.server.backends.pluggable.BackendImpl.Cursor; |
| | | import org.opends.server.backends.pluggable.BackendImpl.Storage; |
| | | import org.opends.server.backends.pluggable.BackendImpl.StorageRuntimeException; |
| | | import org.opends.server.types.DN; |
| | | import org.opends.server.types.Entry; |
| | | import org.opends.server.types.LDIFExportConfig; |
| | | import org.opends.server.util.LDIFException; |
| | | import org.opends.server.util.StaticUtils; |
| | | |
| | | import static org.opends.messages.JebMessages.*; |
| | | |
| | | /** |
| | | * Export a JE backend to LDIF. |
| | | */ |
| | | public class ExportJob |
| | | { |
| | | private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); |
| | | |
| | | |
| | | /** |
| | | * The requested LDIF export configuration. |
| | | */ |
| | | private LDIFExportConfig exportConfig; |
| | | |
| | | /** |
| | | * The number of milliseconds between job progress reports. |
| | | */ |
| | | private long progressInterval = 10000; |
| | | |
| | | /** |
| | | * The current number of entries exported. |
| | | */ |
| | | private long exportedCount = 0; |
| | | |
| | | /** |
| | | * The current number of entries skipped. |
| | | */ |
| | | private long skippedCount = 0; |
| | | |
| | | /** |
| | | * Create a new export job. |
| | | * |
| | | * @param exportConfig The requested LDIF export configuration. |
| | | */ |
| | | public ExportJob(LDIFExportConfig exportConfig) |
| | | { |
| | | this.exportConfig = exportConfig; |
| | | } |
| | | |
| | | /** |
| | | * Export entries from the backend to an LDIF file. |
| | | * @param rootContainer The root container to export. |
| | | * @throws StorageRuntimeException If an error occurs in the JE database. |
| | | * @throws IOException If an I/O error occurs while writing an entry. |
| | | * @throws JebException If an error occurs in the JE backend. |
| | | * @throws LDIFException If an error occurs while trying to determine whether |
| | | * to write an entry. |
| | | */ |
| | | public void exportLDIF(RootContainer rootContainer) |
| | | throws IOException, LDIFException, StorageRuntimeException, JebException |
| | | { |
| | | List<DN> includeBranches = exportConfig.getIncludeBranches(); |
| | | DN baseDN; |
| | | ArrayList<EntryContainer> exportContainers = |
| | | new ArrayList<EntryContainer>(); |
| | | |
| | | for (EntryContainer entryContainer : rootContainer.getEntryContainers()) |
| | | { |
| | | // Skip containers that are not covered by the include branches. |
| | | baseDN = entryContainer.getBaseDN(); |
| | | |
| | | if (includeBranches == null || includeBranches.isEmpty()) |
| | | { |
| | | exportContainers.add(entryContainer); |
| | | } |
| | | else |
| | | { |
| | | for (DN includeBranch : includeBranches) |
| | | { |
| | | if (includeBranch.isDescendantOf(baseDN) || |
| | | includeBranch.isAncestorOf(baseDN)) |
| | | { |
| | | exportContainers.add(entryContainer); |
| | | break; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | // Make a note of the time we started. |
| | | long startTime = System.currentTimeMillis(); |
| | | |
| | | // Start a timer for the progress report. |
| | | Timer timer = new Timer(); |
| | | TimerTask progressTask = new ProgressTask(); |
| | | timer.scheduleAtFixedRate(progressTask, progressInterval, |
| | | progressInterval); |
| | | |
| | | // Iterate through the containers. |
| | | try |
| | | { |
| | | for (EntryContainer exportContainer : exportContainers) |
| | | { |
| | | if (exportConfig.isCancelled()) |
| | | { |
| | | break; |
| | | } |
| | | |
| | | exportContainer.sharedLock.lock(); |
| | | try |
| | | { |
| | | exportContainer(exportContainer); |
| | | } |
| | | finally |
| | | { |
| | | exportContainer.sharedLock.unlock(); |
| | | } |
| | | } |
| | | } |
| | | finally |
| | | { |
| | | timer.cancel(); |
| | | } |
| | | |
| | | |
| | | long finishTime = System.currentTimeMillis(); |
| | | long totalTime = (finishTime - startTime); |
| | | |
| | | float rate = 0; |
| | | if (totalTime > 0) |
| | | { |
| | | rate = 1000f*exportedCount / totalTime; |
| | | } |
| | | |
| | | logger.info(NOTE_JEB_EXPORT_FINAL_STATUS, exportedCount, skippedCount, totalTime/1000, rate); |
| | | |
| | | } |
| | | |
| | | /** |
| | | * Export the entries in a single entry entryContainer, in other words from |
| | | * one of the base DNs. |
| | | * @param entryContainer The entry container that holds the entries to be |
| | | * exported. |
| | | * @throws StorageRuntimeException If an error occurs in the JE database. |
| | | * @throws IOException If an error occurs while writing an entry. |
| | | * @throws LDIFException If an error occurs while trying to determine |
| | | * whether to write an entry. |
| | | */ |
| | | private void exportContainer(EntryContainer entryContainer) |
| | | throws StorageRuntimeException, IOException, LDIFException |
| | | { |
| | | Storage storage = entryContainer.getStorage(); |
| | | |
| | | Cursor cursor = storage.openCursor(entryContainer.getID2Entry().getName()); |
| | | try |
| | | { |
| | | while (cursor.next()) |
| | | { |
| | | if (exportConfig.isCancelled()) |
| | | { |
| | | break; |
| | | } |
| | | |
| | | ByteString key=cursor.getKey(); |
| | | EntryID entryID = null; |
| | | try |
| | | { |
| | | entryID = new EntryID(key); |
| | | } |
| | | catch (Exception e) |
| | | { |
| | | if (logger.isTraceEnabled()) |
| | | { |
| | | logger.traceException(e); |
| | | |
| | | logger.trace("Malformed id2entry ID %s.%n", |
| | | StaticUtils.bytesToHex(key.toByteArray())); |
| | | } |
| | | skippedCount++; |
| | | continue; |
| | | } |
| | | |
| | | if (entryID.longValue() == 0) |
| | | { |
| | | // This is the stored entry count. |
| | | continue; |
| | | } |
| | | |
| | | ByteString value = cursor.getValue(); |
| | | Entry entry = null; |
| | | try |
| | | { |
| | | entry = ID2Entry.entryFromDatabase(value, |
| | | entryContainer.getRootContainer().getCompressedSchema()); |
| | | } |
| | | catch (Exception e) |
| | | { |
| | | if (logger.isTraceEnabled()) |
| | | { |
| | | logger.traceException(e); |
| | | |
| | | logger.trace("Malformed id2entry record for ID %d:%n%s%n", |
| | | entryID.longValue(), |
| | | StaticUtils.bytesToHex(value.toByteArray())); |
| | | } |
| | | skippedCount++; |
| | | continue; |
| | | } |
| | | |
| | | if (entry.toLDIF(exportConfig)) |
| | | { |
| | | exportedCount++; |
| | | } |
| | | else |
| | | { |
| | | skippedCount++; |
| | | } |
| | | } |
| | | } |
| | | finally |
| | | { |
| | | cursor.close(); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * This class reports progress of the export job at fixed intervals. |
| | | */ |
| | | class ProgressTask extends TimerTask |
| | | { |
| | | /** |
| | | * The number of entries that had been exported at the time of the |
| | | * previous progress report. |
| | | */ |
| | | private long previousCount = 0; |
| | | |
| | | /** |
| | | * The time in milliseconds of the previous progress report. |
| | | */ |
| | | private long previousTime; |
| | | |
| | | /** |
| | | * Create a new export progress task. |
| | | */ |
| | | public ProgressTask() |
| | | { |
| | | previousTime = System.currentTimeMillis(); |
| | | } |
| | | |
| | | /** |
| | | * The action to be performed by this timer task. |
| | | */ |
| | | @Override |
| | | public void run() |
| | | { |
| | | long latestCount = exportedCount; |
| | | long deltaCount = (latestCount - previousCount); |
| | | long latestTime = System.currentTimeMillis(); |
| | | long deltaTime = latestTime - previousTime; |
| | | |
| | | if (deltaTime == 0) |
| | | { |
| | | return; |
| | | } |
| | | |
| | | float rate = 1000f*deltaCount / deltaTime; |
| | | |
| | | logger.info(NOTE_JEB_EXPORT_PROGRESS_REPORT, latestCount, skippedCount, rate); |
| | | |
| | | previousCount = latestCount; |
| | | previousTime = latestTime; |
| | | } |
| | | } |
| | | |
| | | } |
| New file |
| | |
| | | /* |
| | | * CDDL HEADER START |
| | | * |
| | | * The contents of this file are subject to the terms of the |
| | | * Common Development and Distribution License, Version 1.0 only |
| | | * (the "License"). You may not use this file except in compliance |
| | | * with the License. |
| | | * |
| | | * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt |
| | | * or http://forgerock.org/license/CDDLv1.0.html. |
| | | * See the License for the specific language governing permissions |
| | | * and limitations under the License. |
| | | * |
| | | * When distributing Covered Code, include this CDDL HEADER in each |
| | | * file and include the License file at legal-notices/CDDLv1_0.txt. |
| | | * If applicable, add the following below this CDDL HEADER, with the |
| | | * fields enclosed by brackets "[]" replaced with your own identifying |
| | | * information: |
| | | * Portions Copyright [yyyy] [name of copyright owner] |
| | | * |
| | | * CDDL HEADER END |
| | | * |
| | | * |
| | | * Copyright 2006-2008 Sun Microsystems, Inc. |
| | | * Portions Copyright 2014 ForgeRock AS |
| | | */ |
| | | package org.opends.server.backends.pluggable; |
| | | |
| | | import java.util.ArrayList; |
| | | import java.util.Iterator; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import java.util.Set; |
| | | |
| | | import org.forgerock.opendj.ldap.ByteString; |
| | | import org.forgerock.opendj.ldap.spi.IndexingOptions; |
| | | import org.opends.server.types.Entry; |
| | | import org.opends.server.types.Modification; |
| | | |
| | | /** |
| | | * Implementation of an Indexer for the children index. |
| | | */ |
| | | public class ID2CIndexer extends Indexer |
| | | { |
| | | /** |
| | | * Create a new indexer for a children index. |
| | | */ |
| | | public ID2CIndexer() |
| | | { |
| | | // No implementation required. |
| | | } |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public String toString() |
| | | { |
| | | return "id2children"; |
| | | } |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public void indexEntry(Entry entry, Set<ByteString> addKeys, IndexingOptions options) |
| | | { |
| | | // The superior entry IDs are in the entry attachment. |
| | | ArrayList<EntryID> ids = (ArrayList<EntryID>) entry.getAttachment(); |
| | | |
| | | // Skip the entry's own ID. |
| | | Iterator<EntryID> iter = ids.iterator(); |
| | | iter.next(); |
| | | |
| | | // Get the parent ID. |
| | | if (iter.hasNext()) |
| | | { |
| | | addKeys.add(iter.next().toByteString()); |
| | | } |
| | | } |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public void replaceEntry(Entry oldEntry, Entry newEntry, |
| | | Map<ByteString, Boolean> modifiedKeys, IndexingOptions options) |
| | | { |
| | | // Nothing to do. |
| | | } |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public void modifyEntry(Entry oldEntry, Entry newEntry, |
| | | List<Modification> mods, |
| | | Map<ByteString, Boolean> modifiedKeys, IndexingOptions options) |
| | | { |
| | | // Nothing to do. |
| | | } |
| | | } |
| New file |
| | |
| | | /* |
| | | * CDDL HEADER START |
| | | * |
| | | * The contents of this file are subject to the terms of the |
| | | * Common Development and Distribution License, Version 1.0 only |
| | | * (the "License"). You may not use this file except in compliance |
| | | * with the License. |
| | | * |
| | | * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt |
| | | * or http://forgerock.org/license/CDDLv1.0.html. |
| | | * See the License for the specific language governing permissions |
| | | * and limitations under the License. |
| | | * |
| | | * When distributing Covered Code, include this CDDL HEADER in each |
| | | * file and include the License file at legal-notices/CDDLv1_0.txt. |
| | | * If applicable, add the following below this CDDL HEADER, with the |
| | | * fields enclosed by brackets "[]" replaced with your own identifying |
| | | * information: |
| | | * Portions Copyright [yyyy] [name of copyright owner] |
| | | * |
| | | * CDDL HEADER END |
| | | * |
| | | * |
| | | * Copyright 2006-2010 Sun Microsystems, Inc. |
| | | * Portions Copyright 2012-2014 ForgeRock AS. |
| | | */ |
| | | package org.opends.server.backends.pluggable; |
| | | |
| | | import java.io.IOException; |
| | | import java.io.OutputStream; |
| | | import java.util.zip.DataFormatException; |
| | | import java.util.zip.DeflaterOutputStream; |
| | | import java.util.zip.InflaterOutputStream; |
| | | |
| | | import org.forgerock.i18n.slf4j.LocalizedLogger; |
| | | import org.forgerock.opendj.io.ASN1; |
| | | import org.forgerock.opendj.io.ASN1Reader; |
| | | import org.forgerock.opendj.io.ASN1Writer; |
| | | import org.forgerock.opendj.ldap.ByteSequence; |
| | | import org.forgerock.opendj.ldap.ByteString; |
| | | import org.forgerock.opendj.ldap.ByteStringBuilder; |
| | | import org.forgerock.opendj.ldap.DecodeException; |
| | | import org.opends.server.api.CompressedSchema; |
| | | import org.opends.server.backends.pluggable.BackendImpl.ReadableStorage; |
| | | import org.opends.server.backends.pluggable.BackendImpl.Storage; |
| | | import org.opends.server.backends.pluggable.BackendImpl.StorageRuntimeException; |
| | | import org.opends.server.backends.pluggable.BackendImpl.TreeName; |
| | | import org.opends.server.backends.pluggable.BackendImpl.WriteableStorage; |
| | | import org.opends.server.core.DirectoryServer; |
| | | import org.opends.server.types.DirectoryException; |
| | | import org.opends.server.types.Entry; |
| | | import org.opends.server.types.LDAPException; |
| | | |
| | | import static org.forgerock.util.Utils.*; |
| | | import static org.opends.messages.JebMessages.*; |
| | | import static org.opends.server.core.DirectoryServer.*; |
| | | |
| | | /** |
| | | * Represents the database containing the LDAP entries. The database key is |
| | | * the entry ID and the value is the entry contents. |
| | | */ |
| | | public class ID2Entry extends DatabaseContainer |
| | | { |
| | | private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); |
| | | |
| | | /** Parameters for compression and encryption. */ |
| | | private DataConfig dataConfig; |
| | | |
| | | /** Cached encoding buffers. */ |
| | | private static final ThreadLocal<EntryCodec> ENTRY_CODEC_CACHE = new ThreadLocal<EntryCodec>() |
| | | { |
| | | @Override |
| | | protected EntryCodec initialValue() |
| | | { |
| | | return new EntryCodec(); |
| | | } |
| | | }; |
| | | |
| | | private static EntryCodec acquireEntryCodec() |
| | | { |
| | | EntryCodec codec = ENTRY_CODEC_CACHE.get(); |
| | | if (codec.maxBufferSize != getMaxInternalBufferSize()) |
| | | { |
| | | // Setting has changed, so recreate the codec. |
| | | codec = new EntryCodec(); |
| | | ENTRY_CODEC_CACHE.set(codec); |
| | | } |
| | | return codec; |
| | | } |
| | | |
| | | /** |
| | | * A cached set of ByteStringBuilder buffers and ASN1Writer used to encode |
| | | * entries. |
| | | */ |
| | | private static class EntryCodec |
| | | { |
| | | private static final int BUFFER_INIT_SIZE = 512; |
| | | |
| | | private final ByteStringBuilder encodedBuffer = new ByteStringBuilder(); |
| | | private final ByteStringBuilder entryBuffer = new ByteStringBuilder(); |
| | | private final ByteStringBuilder compressedEntryBuffer = new ByteStringBuilder(); |
| | | private final ASN1Writer writer; |
| | | private final int maxBufferSize; |
| | | |
| | | private EntryCodec() |
| | | { |
| | | this.maxBufferSize = getMaxInternalBufferSize(); |
| | | this.writer = ASN1.getWriter(encodedBuffer, maxBufferSize); |
| | | } |
| | | |
| | | private void release() |
| | | { |
| | | closeSilently(writer); |
| | | encodedBuffer.clearAndTruncate(maxBufferSize, BUFFER_INIT_SIZE); |
| | | entryBuffer.clearAndTruncate(maxBufferSize, BUFFER_INIT_SIZE); |
| | | compressedEntryBuffer.clearAndTruncate(maxBufferSize, BUFFER_INIT_SIZE); |
| | | } |
| | | |
| | | private Entry decode(ByteString bytes, CompressedSchema compressedSchema) |
| | | throws DirectoryException, DecodeException, LDAPException, |
| | | DataFormatException, IOException |
| | | { |
| | | // Get the format version. |
| | | byte formatVersion = bytes.byteAt(0); |
| | | if(formatVersion != JebFormat.FORMAT_VERSION) |
| | | { |
| | | throw DecodeException.error(ERR_JEB_INCOMPATIBLE_ENTRY_VERSION.get(formatVersion)); |
| | | } |
| | | |
| | | // Read the ASN1 sequence. |
| | | ASN1Reader reader = ASN1.getReader(bytes.subSequence(1, bytes.length())); |
| | | reader.readStartSequence(); |
| | | |
| | | // See if it was compressed. |
| | | int uncompressedSize = (int)reader.readInteger(); |
| | | if(uncompressedSize > 0) |
| | | { |
| | | // It was compressed. |
| | | reader.readOctetString(compressedEntryBuffer); |
| | | |
| | | OutputStream decompressor = null; |
| | | try |
| | | { |
| | | // TODO: Should handle the case where uncompress fails |
| | | decompressor = new InflaterOutputStream(entryBuffer.asOutputStream()); |
| | | compressedEntryBuffer.copyTo(decompressor); |
| | | } |
| | | finally { |
| | | closeSilently(decompressor); |
| | | } |
| | | |
| | | // Since we are used the cached buffers (ByteStringBuilders), |
| | | // the decoded attribute values will not refer back to the |
| | | // original buffer. |
| | | return Entry.decode(entryBuffer.asReader(), compressedSchema); |
| | | } |
| | | else |
| | | { |
| | | // Since we don't have to do any decompression, we can just decode |
| | | // the entry directly. |
| | | ByteString encodedEntry = reader.readOctetString(); |
| | | return Entry.decode(encodedEntry.asReader(), compressedSchema); |
| | | } |
| | | } |
| | | |
| | | private ByteString encodeCopy(Entry entry, DataConfig dataConfig) |
| | | throws DirectoryException |
| | | { |
| | | encodeVolatile(entry, dataConfig); |
| | | return encodedBuffer.toByteString(); |
| | | } |
| | | |
| | | private ByteString encodeInternal(Entry entry, DataConfig dataConfig) |
| | | throws DirectoryException |
| | | { |
| | | encodeVolatile(entry, dataConfig); |
| | | return encodedBuffer.toByteString(); |
| | | } |
| | | |
| | | private void encodeVolatile(Entry entry, DataConfig dataConfig) throws DirectoryException |
| | | { |
| | | // Encode the entry for later use. |
| | | entry.encode(entryBuffer, dataConfig.getEntryEncodeConfig()); |
| | | |
| | | // First write the DB format version byte. |
| | | encodedBuffer.append(JebFormat.FORMAT_VERSION); |
| | | |
| | | try |
| | | { |
| | | // Then start the ASN1 sequence. |
| | | writer.writeStartSequence(JebFormat.TAG_DATABASE_ENTRY); |
| | | |
| | | if (dataConfig.isCompressed()) |
| | | { |
| | | OutputStream compressor = null; |
| | | try { |
| | | compressor = new DeflaterOutputStream(compressedEntryBuffer.asOutputStream()); |
| | | entryBuffer.copyTo(compressor); |
| | | } |
| | | finally { |
| | | closeSilently(compressor); |
| | | } |
| | | |
| | | // Compression needed and successful. |
| | | writer.writeInteger(entryBuffer.length()); |
| | | writer.writeOctetString(compressedEntryBuffer); |
| | | } |
| | | else |
| | | { |
| | | writer.writeInteger(0); |
| | | writer.writeOctetString(entryBuffer); |
| | | } |
| | | |
| | | writer.writeEndSequence(); |
| | | } |
| | | catch(IOException ioe) |
| | | { |
| | | // TODO: This should never happen with byte buffer. |
| | | logger.traceException(ioe); |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Create a new ID2Entry object. |
| | | * |
| | | * @param name The name of the entry database. |
| | | * @param dataConfig The desired compression and encryption options for data |
| | | * stored in the entry database. |
| | | * @param storage The JE Storage. |
| | | * @param entryContainer The entryContainer of the entry database. |
| | | * @throws StorageRuntimeException If an error occurs in the JE database. |
| | | * |
| | | */ |
| | | ID2Entry(TreeName name, DataConfig dataConfig, Storage storage, EntryContainer entryContainer) |
| | | throws StorageRuntimeException |
| | | { |
| | | super(name, storage, entryContainer); |
| | | this.dataConfig = dataConfig; |
| | | } |
| | | |
| | | /** |
| | | * Decodes an entry from its database representation. |
| | | * <p> |
| | | * An entry on disk is ASN1 encoded in this format: |
| | | * |
| | | * <pre> |
| | | * ByteString ::= [APPLICATION 0] IMPLICIT SEQUENCE { |
| | | * uncompressedSize INTEGER, -- A zero value means not compressed. |
| | | * dataBytes OCTET STRING -- Optionally compressed encoding of |
| | | * the data bytes. |
| | | * } |
| | | * |
| | | * ID2EntryValue ::= ByteString |
| | | * -- Where dataBytes contains an encoding of DirectoryServerEntry. |
| | | * |
| | | * DirectoryServerEntry ::= [APPLICATION 1] IMPLICIT SEQUENCE { |
| | | * dn LDAPDN, |
| | | * objectClasses SET OF LDAPString, |
| | | * userAttributes AttributeList, |
| | | * operationalAttributes AttributeList |
| | | * } |
| | | * </pre> |
| | | * |
| | | * @param bytes A byte array containing the encoded database value. |
| | | * @param compressedSchema The compressed schema manager to use when decoding. |
| | | * @return The decoded entry. |
| | | * @throws DecodeException If the data is not in the expected ASN.1 encoding |
| | | * format. |
| | | * @throws LDAPException If the data is not in the expected ASN.1 encoding |
| | | * format. |
| | | * @throws DataFormatException If an error occurs while trying to decompress |
| | | * compressed data. |
| | | * @throws DirectoryException If a Directory Server error occurs. |
| | | * @throws IOException if an error occurs while reading the ASN1 sequence. |
| | | */ |
| | | public static Entry entryFromDatabase(ByteString bytes, |
| | | CompressedSchema compressedSchema) throws DirectoryException, |
| | | DecodeException, LDAPException, DataFormatException, IOException |
| | | { |
| | | EntryCodec codec = acquireEntryCodec(); |
| | | try |
| | | { |
| | | return codec.decode(bytes, compressedSchema); |
| | | } |
| | | finally |
| | | { |
| | | codec.release(); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Encodes an entry to the raw database format, with optional compression. |
| | | * |
| | | * @param entry The entry to encode. |
| | | * @param dataConfig Compression and cryptographic options. |
| | | * @return A ByteSTring containing the encoded database value. |
| | | * |
| | | * @throws DirectoryException If a problem occurs while attempting to encode |
| | | * the entry. |
| | | */ |
| | | public static ByteString entryToDatabase(Entry entry, DataConfig dataConfig) |
| | | throws DirectoryException |
| | | { |
| | | EntryCodec codec = acquireEntryCodec(); |
| | | try |
| | | { |
| | | return codec.encodeCopy(entry, dataConfig); |
| | | } |
| | | finally |
| | | { |
| | | codec.release(); |
| | | } |
| | | } |
| | | |
| | | |
| | | |
| | | /** |
| | | * Insert a record into the entry database. |
| | | * |
| | | * @param txn The database transaction or null if none. |
| | | * @param id The entry ID which forms the key. |
| | | * @param entry The LDAP entry. |
| | | * @return true if the entry was inserted, false if a record with that |
| | | * ID already existed. |
| | | * @throws StorageRuntimeException If an error occurs in the JE database. |
| | | * @throws DirectoryException If a problem occurs while attempting to encode |
| | | * the entry. |
| | | */ |
| | | public boolean insert(WriteableStorage txn, EntryID id, Entry entry) |
| | | throws StorageRuntimeException, DirectoryException |
| | | { |
| | | ByteString key = id.toByteString(); |
| | | EntryCodec codec = acquireEntryCodec(); |
| | | try |
| | | { |
| | | ByteString value = codec.encodeInternal(entry, dataConfig); |
| | | return insert(txn, key, value); |
| | | } |
| | | finally |
| | | { |
| | | codec.release(); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Write a record in the entry database. |
| | | * |
| | | * @param txn The database transaction or null if none. |
| | | * @param id The entry ID which forms the key. |
| | | * @param entry The LDAP entry. |
| | | * @throws StorageRuntimeException If an error occurs in the JE database. |
| | | * @throws DirectoryException If a problem occurs while attempting to encode |
| | | * the entry. |
| | | */ |
| | | public void put(WriteableStorage txn, EntryID id, Entry entry) |
| | | throws StorageRuntimeException, DirectoryException |
| | | { |
| | | ByteString key = id.toByteString(); |
| | | EntryCodec codec = acquireEntryCodec(); |
| | | try |
| | | { |
| | | ByteString value = codec.encodeInternal(entry, dataConfig); |
| | | put(txn, key, value); |
| | | } |
| | | finally |
| | | { |
| | | codec.release(); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Write a pre-formatted record into the entry database. |
| | | * |
| | | * @param txn The database transaction or null if none. |
| | | * @param key The key containing a pre-formatted entry ID. |
| | | * @param value The data value containing a pre-formatted LDAP entry. |
| | | * @throws StorageRuntimeException If an error occurs in the JE database. |
| | | */ |
| | | @Override |
| | | public void put(WriteableStorage txn, ByteSequence key, ByteSequence value) throws StorageRuntimeException |
| | | { |
| | | super.put(txn, key, value); |
| | | } |
| | | |
| | | /** |
| | | * Remove a record from the entry database. |
| | | * |
| | | * @param txn The database transaction or null if none. |
| | | * @param id The entry ID which forms the key. |
| | | * @return true if the entry was removed, false if it was not. |
| | | * @throws StorageRuntimeException If an error occurs in the JE database. |
| | | */ |
| | | public boolean remove(WriteableStorage txn, EntryID id) throws StorageRuntimeException |
| | | { |
| | | return delete(txn, id.toByteString()); |
| | | } |
| | | |
| | | /** |
| | | * Fetch a record from the entry database. |
| | | * |
| | | * @param txn The database transaction or null if none. |
| | | * @param id The desired entry ID which forms the key. |
| | | * @return The requested entry, or null if there is no such record. |
| | | * @throws DirectoryException If a problem occurs while getting the entry. |
| | | * @throws StorageRuntimeException If an error occurs in the JE database. |
| | | */ |
| | | public Entry get(ReadableStorage txn, EntryID id, boolean isRMW) |
| | | throws DirectoryException, StorageRuntimeException |
| | | { |
| | | ByteString value = read(txn, id.toByteString(), isRMW); |
| | | if (value == null) |
| | | { |
| | | return null; |
| | | } |
| | | |
| | | try |
| | | { |
| | | Entry entry = entryFromDatabase(value, |
| | | entryContainer.getRootContainer().getCompressedSchema()); |
| | | entry.processVirtualAttributes(); |
| | | return entry; |
| | | } |
| | | catch (Exception e) |
| | | { |
| | | throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), |
| | | ERR_JEB_ENTRY_DATABASE_CORRUPT.get(id)); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Set the desired compression and encryption options for data |
| | | * stored in the entry database. |
| | | * |
| | | * @param dataConfig The desired compression and encryption options for data |
| | | * stored in the entry database. |
| | | */ |
| | | public void setDataConfig(DataConfig dataConfig) |
| | | { |
| | | this.dataConfig = dataConfig; |
| | | } |
| | | } |
| New file |
| | |
| | | /* |
| | | * CDDL HEADER START |
| | | * |
| | | * The contents of this file are subject to the terms of the |
| | | * Common Development and Distribution License, Version 1.0 only |
| | | * (the "License"). You may not use this file except in compliance |
| | | * with the License. |
| | | * |
| | | * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt |
| | | * or http://forgerock.org/license/CDDLv1.0.html. |
| | | * See the License for the specific language governing permissions |
| | | * and limitations under the License. |
| | | * |
| | | * When distributing Covered Code, include this CDDL HEADER in each |
| | | * file and include the License file at legal-notices/CDDLv1_0.txt. |
| | | * If applicable, add the following below this CDDL HEADER, with the |
| | | * fields enclosed by brackets "[]" replaced with your own identifying |
| | | * information: |
| | | * Portions Copyright [yyyy] [name of copyright owner] |
| | | * |
| | | * CDDL HEADER END |
| | | * |
| | | * |
| | | * Copyright 2006-2008 Sun Microsystems, Inc. |
| | | * Portions Copyright 2014 ForgeRock AS |
| | | */ |
| | | package org.opends.server.backends.pluggable; |
| | | |
| | | import java.util.*; |
| | | |
| | | import org.forgerock.opendj.ldap.ByteString; |
| | | import org.forgerock.opendj.ldap.spi.IndexingOptions; |
| | | import org.opends.server.types.Entry; |
| | | import org.opends.server.types.Modification; |
| | | |
| | | |
| | | |
| | | /** |
| | | * Implementation of an Indexer for the subtree index. |
| | | */ |
| | | public class ID2SIndexer extends Indexer |
| | | { |
| | | /** |
| | | * Create a new indexer for a subtree index. |
| | | */ |
| | | public ID2SIndexer() |
| | | { |
| | | } |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public String toString() |
| | | { |
| | | return "id2subtree"; |
| | | } |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public void indexEntry(Entry entry, Set<ByteString> addKeys, IndexingOptions options) |
| | | { |
| | | // The superior entry IDs are in the entry attachment. |
| | | ArrayList<EntryID> ids = (ArrayList<EntryID>) entry.getAttachment(); |
| | | |
| | | // Skip the entry's own ID. |
| | | Iterator<EntryID> iter = ids.iterator(); |
| | | iter.next(); |
| | | |
| | | // Iterate through the superior IDs. |
| | | while (iter.hasNext()) |
| | | { |
| | | ByteString nodeIDData = iter.next().toByteString(); |
| | | addKeys.add(nodeIDData); |
| | | } |
| | | } |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public void replaceEntry(Entry oldEntry, Entry newEntry, |
| | | Map<ByteString, Boolean> modifiedKeys, IndexingOptions options) |
| | | { |
| | | // Nothing to do. |
| | | } |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public void modifyEntry(Entry oldEntry, Entry newEntry, |
| | | List<Modification> mods, |
| | | Map<ByteString, Boolean> modifiedKeys, IndexingOptions options) |
| | | { |
| | | // Nothing to do. |
| | | } |
| | | } |
| New file |
| | |
| | | /* |
| | | * CDDL HEADER START |
| | | * |
| | | * The contents of this file are subject to the terms of the |
| | | * Common Development and Distribution License, Version 1.0 only |
| | | * (the "License"). You may not use this file except in compliance |
| | | * with the License. |
| | | * |
| | | * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt |
| | | * or http://forgerock.org/license/CDDLv1.0.html. |
| | | * See the License for the specific language governing permissions |
| | | * and limitations under the License. |
| | | * |
| | | * When distributing Covered Code, include this CDDL HEADER in each |
| | | * file and include the License file at legal-notices/CDDLv1_0.txt. |
| | | * If applicable, add the following below this CDDL HEADER, with the |
| | | * fields enclosed by brackets "[]" replaced with your own identifying |
| | | * information: |
| | | * Portions Copyright [yyyy] [name of copyright owner] |
| | | * |
| | | * CDDL HEADER END |
| | | * |
| | | * |
| | | * Copyright 2006-2008 Sun Microsystems, Inc. |
| | | * Portions Copyright 2014 ForgeRock AS |
| | | */ |
| | | package org.opends.server.backends.pluggable; |
| | | |
| | | import java.util.Iterator; |
| | | import java.util.NoSuchElementException; |
| | | |
| | | /** |
| | | * Iterator for a set of Entry IDs. It must return values in order of ID. |
| | | */ |
| | | public class IDSetIterator implements Iterator<EntryID> |
| | | { |
| | | /** |
| | | * An array of ID values in order of ID. |
| | | */ |
| | | private long[] entryIDList; |
| | | |
| | | /** |
| | | * Current position of the iterator as an index into the array of IDs. |
| | | */ |
| | | private int i; |
| | | |
| | | /** |
| | | * Create a new iterator for a given array of entry IDs. |
| | | * @param entryIDList An array of IDs in order or ID. |
| | | */ |
| | | public IDSetIterator(long[] entryIDList) |
| | | { |
| | | this.entryIDList = entryIDList; |
| | | } |
| | | |
| | | /** |
| | | * Create a new iterator for a given array of entry IDs. |
| | | * @param entryIDList An array of IDs in order or ID. |
| | | * @param begin The entry ID of the first entry that should be returned, or |
| | | * {@code null} if it should start at the beginning of the list. |
| | | */ |
| | | public IDSetIterator(long[] entryIDList, EntryID begin) |
| | | { |
| | | this.entryIDList = entryIDList; |
| | | |
| | | if (begin == null) |
| | | { |
| | | i = 0; |
| | | } |
| | | else |
| | | { |
| | | for (i=0; i < entryIDList.length; i++) |
| | | { |
| | | if (entryIDList[i] == begin.longValue()) |
| | | { |
| | | break; |
| | | } |
| | | } |
| | | |
| | | if (i >= entryIDList.length) |
| | | { |
| | | i = 0; |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Returns <tt>true</tt> if the iteration has more elements. (In other |
| | | * words, returns <tt>true</tt> if <tt>next</tt> would return an element |
| | | * rather than throwing an exception.) |
| | | * |
| | | * @return <tt>true</tt> if the iterator has more elements. |
| | | */ |
| | | public boolean hasNext() |
| | | { |
| | | return i < entryIDList.length; |
| | | } |
| | | |
| | | /** |
| | | * Returns the next element in the iteration. Calling this method |
| | | * repeatedly until the {@link #hasNext()} method returns false will |
| | | * return each element in the underlying collection exactly once. |
| | | * |
| | | * @return the next element in the iteration. |
| | | * @throws java.util.NoSuchElementException |
| | | * iteration has no more elements. |
| | | */ |
| | | public EntryID next() |
| | | throws NoSuchElementException |
| | | { |
| | | if (i < entryIDList.length) |
| | | { |
| | | return new EntryID(entryIDList[i++]); |
| | | } |
| | | throw new NoSuchElementException(); |
| | | } |
| | | |
| | | /** |
| | | * |
| | | * Removes from the underlying collection the last element returned by the |
| | | * iterator (optional operation). This method can be called only once per |
| | | * call to <tt>next</tt>. The behavior of an iterator is unspecified if |
| | | * the underlying collection is modified while the iteration is in |
| | | * progress in any way other than by calling this method. |
| | | * |
| | | * @exception UnsupportedOperationException if the <tt>remove</tt> |
| | | * operation is not supported by this Iterator. |
| | | */ |
| | | public void remove() throws UnsupportedOperationException |
| | | { |
| | | throw new UnsupportedOperationException(); |
| | | } |
| | | } |
| New file |
| | |
| | | /* |
| | | * CDDL HEADER START |
| | | * |
| | | * The contents of this file are subject to the terms of the |
| | | * Common Development and Distribution License, Version 1.0 only |
| | | * (the "License"). You may not use this file except in compliance |
| | | * with the License. |
| | | * |
| | | * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt |
| | | * or http://forgerock.org/license/CDDLv1.0.html. |
| | | * See the License for the specific language governing permissions |
| | | * and limitations under the License. |
| | | * |
| | | * When distributing Covered Code, include this CDDL HEADER in each |
| | | * file and include the License file at legal-notices/CDDLv1_0.txt. |
| | | * If applicable, add the following below this CDDL HEADER, with the |
| | | * fields enclosed by brackets "[]" replaced with your own identifying |
| | | * information: |
| | | * Portions Copyright [yyyy] [name of copyright owner] |
| | | * |
| | | * CDDL HEADER END |
| | | * |
| | | * |
| | | * Copyright 2006-2010 Sun Microsystems, Inc. |
| | | * Portions Copyright 2012-2014 ForgeRock AS |
| | | */ |
| | | package org.opends.server.backends.pluggable; |
| | | |
| | | import java.util.ArrayList; |
| | | import java.util.HashSet; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import java.util.TreeMap; |
| | | |
| | | import org.forgerock.i18n.slf4j.LocalizedLogger; |
| | | import org.forgerock.opendj.ldap.ByteSequence; |
| | | import org.forgerock.opendj.ldap.ByteString; |
| | | import org.forgerock.opendj.ldap.ConditionResult; |
| | | import org.forgerock.opendj.ldap.spi.IndexingOptions; |
| | | import org.opends.server.backends.pluggable.IndexBuffer.BufferedIndexValues; |
| | | import org.opends.server.backends.pluggable.BackendImpl.Cursor; |
| | | import org.opends.server.backends.pluggable.BackendImpl.ReadableStorage; |
| | | import org.opends.server.backends.pluggable.BackendImpl.Storage; |
| | | import org.opends.server.backends.pluggable.BackendImpl.StorageRuntimeException; |
| | | import org.opends.server.backends.pluggable.BackendImpl.TreeName; |
| | | import org.opends.server.backends.pluggable.BackendImpl.WriteableStorage; |
| | | import org.opends.server.types.DirectoryException; |
| | | import org.opends.server.types.Entry; |
| | | import org.opends.server.types.Modification; |
| | | import org.opends.server.util.StaticUtils; |
| | | |
| | | import static org.opends.messages.JebMessages.*; |
| | | |
| | | /** |
| | | * Represents an index implemented by a JE database in which each key maps to |
| | | * a set of entry IDs. The key is a byte array, and is constructed from some |
| | | * normalized form of an attribute value (or fragment of a value) appearing |
| | | * in the entry. |
| | | */ |
| | | public class Index extends DatabaseContainer |
| | | { |
| | | private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); |
| | | |
| | | /** The indexer object to construct index keys from LDAP attribute values. */ |
| | | public Indexer indexer; |
| | | |
| | | /** The limit on the number of entry IDs that may be indexed by one key. */ |
| | | private int indexEntryLimit; |
| | | /** |
| | | * Limit on the number of entry IDs that may be retrieved by cursoring |
| | | * through an index. |
| | | */ |
| | | private final int cursorEntryLimit; |
| | | /** |
| | | * Number of keys that have exceeded the entry limit since this |
| | | * object was created. |
| | | */ |
| | | private int entryLimitExceededCount; |
| | | |
| | | /** The max number of tries to rewrite phantom records. */ |
| | | private final int phantomWriteRetries = 3; |
| | | |
| | | /** |
| | | * Whether to maintain a count of IDs for a key once the entry limit |
| | | * has exceeded. |
| | | */ |
| | | private final boolean maintainCount; |
| | | |
| | | private final State state; |
| | | |
| | | /** |
| | | * A flag to indicate if this index should be trusted to be consistent |
| | | * with the entries database. If not trusted, we assume that existing |
| | | * entryIDSets for a key is still accurate. However, keys that do not |
| | | * exist are undefined instead of an empty entryIDSet. The following |
| | | * rules will be observed when the index is not trusted: |
| | | * |
| | | * - no entryIDs will be added to a non-existing key. |
| | | * - undefined entryIdSet will be returned whenever a key is not found. |
| | | */ |
| | | private boolean trusted; |
| | | |
| | | /** |
| | | * A flag to indicate if a rebuild process is running on this index. |
| | | * During the rebuild process, we assume that no entryIDSets are |
| | | * accurate and return an undefined set on all read operations. |
| | | * However all write operations will succeed. The rebuildRunning |
| | | * flag overrides all behaviors of the trusted flag. |
| | | */ |
| | | private boolean rebuildRunning; |
| | | |
| | | /** Thread local area to store per thread cursors. */ |
| | | private final ThreadLocal<Cursor> curLocal = new ThreadLocal<Cursor>(); |
| | | |
| | | /** |
| | | * Create a new index object. |
| | | * @param name The name of the index database within the entryContainer. |
| | | * @param indexer The indexer object to construct index keys from LDAP |
| | | * attribute values. |
| | | * @param state The state database to persist index state info. |
| | | * @param indexEntryLimit The configured limit on the number of entry IDs |
| | | * that may be indexed by one key. |
| | | * @param cursorEntryLimit The configured limit on the number of entry IDs |
| | | * @param maintainCount Whether to maintain a count of IDs for a key once |
| | | * the entry limit has exceeded. |
| | | * @param storage The JE Storage |
| | | * @param entryContainer The database entryContainer holding this index. |
| | | * @throws StorageRuntimeException If an error occurs in the JE database. |
| | | */ |
| | | public Index(TreeName name, Indexer indexer, State state, |
| | | int indexEntryLimit, int cursorEntryLimit, boolean maintainCount, |
| | | Storage storage, EntryContainer entryContainer) |
| | | throws StorageRuntimeException |
| | | { |
| | | super(name, storage, entryContainer); |
| | | this.indexer = indexer; |
| | | this.indexEntryLimit = indexEntryLimit; |
| | | this.cursorEntryLimit = cursorEntryLimit; |
| | | this.maintainCount = maintainCount; |
| | | |
| | | this.state = state; |
| | | this.trusted = state.getIndexTrustState(null, this); |
| | | if (!trusted && entryContainer.getHighestEntryID().longValue() == 0) |
| | | { |
| | | // If there are no entries in the entry container then there |
| | | // is no reason why this index can't be upgraded to trusted. |
| | | setTrusted(null, true); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Add an add entry ID operation into a index buffer. |
| | | * |
| | | * @param buffer The index buffer to insert the ID into. |
| | | * @param keyBytes The index key bytes. |
| | | * @param entryID The entry ID. |
| | | */ |
| | | public void insertID(IndexBuffer buffer, ByteString keyBytes, EntryID entryID) |
| | | { |
| | | getBufferedIndexValues(buffer, keyBytes).addEntryID(keyBytes, entryID); |
| | | } |
| | | |
| | | /** |
| | | * Update the set of entry IDs for a given key. |
| | | * |
| | | * @param txn A database transaction, or null if none is required. |
| | | * @param key The database key. |
| | | * @param deletedIDs The IDs to remove for the key. |
| | | * @param addedIDs the IDs to add for the key. |
| | | * @throws StorageRuntimeException If a database error occurs. |
| | | */ |
| | | void updateKey(WriteableStorage txn, ByteString key, EntryIDSet deletedIDs, EntryIDSet addedIDs) |
| | | throws StorageRuntimeException |
| | | { |
| | | if(deletedIDs == null && addedIDs == null) |
| | | { |
| | | boolean success = delete(txn, key); |
| | | if (success && logger.isTraceEnabled()) |
| | | { |
| | | StringBuilder builder = new StringBuilder(); |
| | | StaticUtils.byteArrayToHexPlusAscii(builder, key.toByteArray(), 4); |
| | | logger.trace("The expected key does not exist in the index %s.\nKey:%s ", treeName, builder); |
| | | } |
| | | return; |
| | | } |
| | | |
| | | // Handle cases where nothing is changed early to avoid DB access. |
| | | if (isNullOrEmpty(deletedIDs) && isNullOrEmpty(addedIDs)) |
| | | { |
| | | return; |
| | | } |
| | | |
| | | if(maintainCount) |
| | | { |
| | | updateKeyWithRMW(txn, key, deletedIDs, addedIDs); |
| | | } |
| | | else |
| | | { |
| | | ByteString value = read(txn, key, false); |
| | | if(value != null) |
| | | { |
| | | EntryIDSet entryIDList = new EntryIDSet(key, value); |
| | | if (entryIDList.isDefined()) |
| | | { |
| | | updateKeyWithRMW(txn, key, deletedIDs, addedIDs); |
| | | } |
| | | } |
| | | else |
| | | { |
| | | if (deletedIDs != null && trusted && !rebuildRunning) |
| | | { |
| | | logIndexCorruptError(txn, key); |
| | | } |
| | | |
| | | if ((rebuildRunning || trusted) && isNotNullOrEmpty(addedIDs)) |
| | | { |
| | | if(!insert(txn, key, addedIDs.toByteString())) |
| | | { |
| | | updateKeyWithRMW(txn, key, deletedIDs, addedIDs); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | private boolean isNullOrEmpty(EntryIDSet entryIDSet) |
| | | { |
| | | return entryIDSet == null || entryIDSet.size() == 0; |
| | | } |
| | | |
| | | private boolean isNotNullOrEmpty(EntryIDSet entryIDSet) |
| | | { |
| | | return entryIDSet != null && entryIDSet.size() > 0; |
| | | } |
| | | |
| | | private void updateKeyWithRMW(WriteableStorage txn, |
| | | ByteString key, |
| | | EntryIDSet deletedIDs, |
| | | EntryIDSet addedIDs) |
| | | throws StorageRuntimeException |
| | | { |
| | | final ByteString value = read(txn, key, false); |
| | | if(value != null) |
| | | { |
| | | EntryIDSet entryIDList = computeEntryIDList(key, value, deletedIDs, addedIDs); |
| | | ByteString after = entryIDList.toByteString(); |
| | | if (after != null) |
| | | { |
| | | put(txn, key, after); |
| | | } |
| | | else |
| | | { |
| | | // No more IDs, so remove the key. If index is not |
| | | // trusted then this will cause all subsequent reads |
| | | // for this key to return undefined set. |
| | | delete(txn, key); |
| | | } |
| | | } |
| | | else |
| | | { |
| | | if (deletedIDs != null && trusted && !rebuildRunning) |
| | | { |
| | | logIndexCorruptError(txn, key); |
| | | } |
| | | |
| | | if ((rebuildRunning || trusted) && isNotNullOrEmpty(addedIDs)) |
| | | { |
| | | insert(txn, key, addedIDs.toByteString()); |
| | | } |
| | | } |
| | | } |
| | | |
| | | private EntryIDSet computeEntryIDList(ByteString key, ByteString value, EntryIDSet deletedIDs, |
| | | EntryIDSet addedIDs) |
| | | { |
| | | EntryIDSet entryIDList = new EntryIDSet(key, value); |
| | | if(addedIDs != null) |
| | | { |
| | | if(entryIDList.isDefined() && indexEntryLimit > 0) |
| | | { |
| | | long idCountDelta = addedIDs.size(); |
| | | if(deletedIDs != null) |
| | | { |
| | | idCountDelta -= deletedIDs.size(); |
| | | } |
| | | if(idCountDelta + entryIDList.size() >= indexEntryLimit) |
| | | { |
| | | if(maintainCount) |
| | | { |
| | | entryIDList = new EntryIDSet(entryIDList.size() + idCountDelta); |
| | | } |
| | | else |
| | | { |
| | | entryIDList = new EntryIDSet(); |
| | | } |
| | | entryLimitExceededCount++; |
| | | |
| | | if(logger.isTraceEnabled()) |
| | | { |
| | | StringBuilder builder = new StringBuilder(); |
| | | StaticUtils.byteArrayToHexPlusAscii(builder, key.toByteArray(), 4); |
| | | logger.trace("Index entry exceeded in index %s. " + |
| | | "Limit: %d. ID list size: %d.\nKey:%s", |
| | | treeName, indexEntryLimit, idCountDelta + addedIDs.size(), builder); |
| | | |
| | | } |
| | | } |
| | | else |
| | | { |
| | | entryIDList.addAll(addedIDs); |
| | | if(deletedIDs != null) |
| | | { |
| | | entryIDList.deleteAll(deletedIDs); |
| | | } |
| | | } |
| | | } |
| | | else |
| | | { |
| | | entryIDList.addAll(addedIDs); |
| | | if(deletedIDs != null) |
| | | { |
| | | entryIDList.deleteAll(deletedIDs); |
| | | } |
| | | } |
| | | } |
| | | else if(deletedIDs != null) |
| | | { |
| | | entryIDList.deleteAll(deletedIDs); |
| | | } |
| | | return entryIDList; |
| | | } |
| | | |
| | | /** |
| | | * Add an remove entry ID operation into a index buffer. |
| | | * |
| | | * @param buffer The index buffer to insert the ID into. |
| | | * @param keyBytes The index key bytes. |
| | | * @param entryID The entry ID. |
| | | */ |
| | | public void removeID(IndexBuffer buffer, ByteString keyBytes, EntryID entryID) |
| | | { |
| | | getBufferedIndexValues(buffer, keyBytes).deleteEntryID(keyBytes, entryID); |
| | | } |
| | | |
| | | private void logIndexCorruptError(WriteableStorage txn, ByteString key) |
| | | { |
| | | if (logger.isTraceEnabled()) |
| | | { |
| | | StringBuilder builder = new StringBuilder(); |
| | | StaticUtils.byteArrayToHexPlusAscii(builder, key.toByteArray(), 4); |
| | | logger.trace("The expected key does not exist in the index %s.\nKey:%s", treeName, builder); |
| | | } |
| | | |
| | | setTrusted(txn, false); |
| | | logger.error(ERR_JEB_INDEX_CORRUPT_REQUIRES_REBUILD, treeName); |
| | | } |
| | | |
| | | /** |
| | | * Buffered delete of a key from the JE database. |
| | | * @param buffer The index buffer to use to store the deleted keys |
| | | * @param keyBytes The index key bytes. |
| | | */ |
| | | public void delete(IndexBuffer buffer, ByteString keyBytes) |
| | | { |
| | | getBufferedIndexValues(buffer, keyBytes); |
| | | } |
| | | |
| | | private BufferedIndexValues getBufferedIndexValues(IndexBuffer buffer, ByteString keyBytes) |
| | | { |
| | | return buffer.getBufferedIndexValues(this, keyBytes); |
| | | } |
| | | |
| | | /** |
| | | * Check if an entry ID is in the set of IDs indexed by a given key. |
| | | * |
| | | * @param txn A database transaction, or null if none is required. |
| | | * @param key The index key. |
| | | * @param entryID The entry ID. |
| | | * @return true if the entry ID is indexed by the given key, |
| | | * false if it is not indexed by the given key, |
| | | * undefined if the key has exceeded the entry limit. |
| | | * @throws StorageRuntimeException If an error occurs in the JE database. |
| | | */ |
| | | public ConditionResult containsID(ReadableStorage txn, ByteString key, EntryID entryID) |
| | | throws StorageRuntimeException |
| | | { |
| | | if(rebuildRunning) |
| | | { |
| | | return ConditionResult.UNDEFINED; |
| | | } |
| | | |
| | | ByteString value = read(txn, key, false); |
| | | if (value != null) |
| | | { |
| | | EntryIDSet entryIDList = new EntryIDSet(key, value); |
| | | if (!entryIDList.isDefined()) |
| | | { |
| | | return ConditionResult.UNDEFINED; |
| | | } |
| | | return ConditionResult.valueOf(entryIDList.contains(entryID)); |
| | | } |
| | | else if (trusted) |
| | | { |
| | | return ConditionResult.FALSE; |
| | | } |
| | | else |
| | | { |
| | | return ConditionResult.UNDEFINED; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Reads the set of entry IDs for a given key. |
| | | * |
| | | * @param key The database key. |
| | | * @param txn A database transaction, or null if none is required. |
| | | * @return The entry IDs indexed by this key. |
| | | */ |
| | | public EntryIDSet readKey(ByteSequence key, ReadableStorage txn) |
| | | { |
| | | if(rebuildRunning) |
| | | { |
| | | return new EntryIDSet(); |
| | | } |
| | | |
| | | try |
| | | { |
| | | ByteString value = read(txn, key, false); |
| | | if (value == null) |
| | | { |
| | | if(trusted) |
| | | { |
| | | return new EntryIDSet(key, null); |
| | | } |
| | | else |
| | | { |
| | | return new EntryIDSet(); |
| | | } |
| | | } |
| | | return new EntryIDSet(key, value); |
| | | } |
| | | catch (StorageRuntimeException e) |
| | | { |
| | | logger.traceException(e); |
| | | return new EntryIDSet(); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Writes the set of entry IDs for a given key. |
| | | * |
| | | * @param key The database key. |
| | | * @param entryIDList The entry IDs indexed by this key. |
| | | * @param txn A database transaction, or null if none is required. |
| | | * @throws StorageRuntimeException If an error occurs in the JE database. |
| | | */ |
| | | public void writeKey(WriteableStorage txn, ByteString key, EntryIDSet entryIDList) |
| | | throws StorageRuntimeException |
| | | { |
| | | ByteString value = entryIDList.toByteString(); |
| | | if (value != null) |
| | | { |
| | | if (!entryIDList.isDefined()) |
| | | { |
| | | entryLimitExceededCount++; |
| | | } |
| | | put(txn, key, value); |
| | | } |
| | | else |
| | | { |
| | | // No more IDs, so remove the key. |
| | | delete(txn, key); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Reads a range of keys and collects all their entry IDs into a |
| | | * single set. |
| | | * |
| | | * @param lower The lower bound of the range. A 0 length byte array indicates |
| | | * no lower bound and the range will start from the |
| | | * smallest key. |
| | | * @param upper The upper bound of the range. A 0 length byte array indicates |
| | | * no upper bound and the range will end at the largest |
| | | * key. |
| | | * @param lowerIncluded true if a key exactly matching the lower bound |
| | | * is included in the range, false if only keys |
| | | * strictly greater than the lower bound are included. |
| | | * This value is ignored if the lower bound is not |
| | | * specified. |
| | | * @param upperIncluded true if a key exactly matching the upper bound |
| | | * is included in the range, false if only keys |
| | | * strictly less than the upper bound are included. |
| | | * This value is ignored if the upper bound is not |
| | | * specified. |
| | | * @return The set of entry IDs. |
| | | */ |
| | | public EntryIDSet readRange(ByteSequence lower, ByteSequence upper, |
| | | boolean lowerIncluded, boolean upperIncluded) |
| | | { |
| | | // If this index is not trusted, then just return an undefined id set. |
| | | if(rebuildRunning || !trusted) |
| | | { |
| | | return new EntryIDSet(); |
| | | } |
| | | |
| | | try |
| | | { |
| | | // Total number of IDs found so far. |
| | | int totalIDCount = 0; |
| | | |
| | | ArrayList<EntryIDSet> lists = new ArrayList<EntryIDSet>(); |
| | | |
| | | Cursor cursor = storage.openCursor(treeName); |
| | | try |
| | | { |
| | | ByteSequence key = ByteString.empty(); |
| | | boolean success; |
| | | // Set the lower bound if necessary. |
| | | if (lower.length() > 0) |
| | | { |
| | | // Initialize the cursor to the lower bound. |
| | | key = lower; |
| | | success = cursor.positionToKeyOrNext(key); |
| | | |
| | | // Advance past the lower bound if necessary. |
| | | if (success |
| | | && !lowerIncluded |
| | | && ByteSequence.COMPARATOR.compare(key, lower) == 0) |
| | | { |
| | | // Do not include the lower value. |
| | | success = cursor.next(); |
| | | if (success) |
| | | { |
| | | key = cursor.getKey(); |
| | | } |
| | | } |
| | | } |
| | | else |
| | | { |
| | | success = cursor.next(); |
| | | if (success) |
| | | { |
| | | key = cursor.getKey(); |
| | | } |
| | | } |
| | | |
| | | if (!success) |
| | | { |
| | | // There are no values. |
| | | return new EntryIDSet(key, null); |
| | | } |
| | | |
| | | // Step through the keys until we hit the upper bound or the last key. |
| | | while (success) |
| | | { |
| | | // Check against the upper bound if necessary |
| | | if (upper.length() > 0) |
| | | { |
| | | int cmp = ByteSequence.COMPARATOR.compare(cursor.getKey(), upper); |
| | | if (cmp > 0 || (cmp == 0 && !upperIncluded)) |
| | | { |
| | | break; |
| | | } |
| | | } |
| | | EntryIDSet list = new EntryIDSet(key, cursor.getValue()); |
| | | if (!list.isDefined()) |
| | | { |
| | | // There is no point continuing. |
| | | return list; |
| | | } |
| | | totalIDCount += list.size(); |
| | | if (cursorEntryLimit > 0 && totalIDCount > cursorEntryLimit) |
| | | { |
| | | // There are too many. Give up and return an undefined list. |
| | | return new EntryIDSet(); |
| | | } |
| | | lists.add(list); |
| | | success = cursor.next(); |
| | | } |
| | | |
| | | return EntryIDSet.unionOfSets(lists, false); |
| | | } |
| | | finally |
| | | { |
| | | cursor.close(); |
| | | } |
| | | } |
| | | catch (StorageRuntimeException e) |
| | | { |
| | | logger.traceException(e); |
| | | return new EntryIDSet(); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Get the number of keys that have exceeded the entry limit since this |
| | | * object was created. |
| | | * @return The number of keys that have exceeded the entry limit since this |
| | | * object was created. |
| | | */ |
| | | public int getEntryLimitExceededCount() |
| | | { |
| | | return entryLimitExceededCount; |
| | | } |
| | | |
| | | /** |
| | | * Close any cursors open against this index. |
| | | * |
| | | * @throws StorageRuntimeException If a database error occurs. |
| | | */ |
| | | public void closeCursor() throws StorageRuntimeException { |
| | | Cursor cursor = curLocal.get(); |
| | | if(cursor != null) { |
| | | cursor.close(); |
| | | curLocal.remove(); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Update the index buffer for a deleted entry. |
| | | * |
| | | * @param buffer The index buffer to use to store the deleted keys |
| | | * @param entryID The entry ID. |
| | | * @param entry The entry to be indexed. |
| | | * @param options The indexing options to use |
| | | * @throws StorageRuntimeException If an error occurs in the JE database. |
| | | * @throws DirectoryException If a Directory Server error occurs. |
| | | */ |
| | | public void addEntry(IndexBuffer buffer, EntryID entryID, Entry entry, |
| | | IndexingOptions options) throws StorageRuntimeException, DirectoryException |
| | | { |
| | | HashSet<ByteString> addKeys = new HashSet<ByteString>(); |
| | | indexer.indexEntry(entry, addKeys, options); |
| | | |
| | | for (ByteString keyBytes : addKeys) |
| | | { |
| | | insertID(buffer, keyBytes, entryID); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Update the index buffer for a deleted entry. |
| | | * |
| | | * @param buffer The index buffer to use to store the deleted keys |
| | | * @param entryID The entry ID |
| | | * @param entry The contents of the deleted entry. |
| | | * @param options The indexing options to use |
| | | * @throws StorageRuntimeException If an error occurs in the JE database. |
| | | * @throws DirectoryException If a Directory Server error occurs. |
| | | */ |
| | | public void removeEntry(IndexBuffer buffer, EntryID entryID, Entry entry, |
| | | IndexingOptions options) throws StorageRuntimeException, DirectoryException |
| | | { |
| | | HashSet<ByteString> delKeys = new HashSet<ByteString>(); |
| | | indexer.indexEntry(entry, delKeys, options); |
| | | |
| | | for (ByteString keyBytes : delKeys) |
| | | { |
| | | removeID(buffer, keyBytes, entryID); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Update the index to reflect a sequence of modifications in a Modify |
| | | * operation. |
| | | * |
| | | * @param buffer The index buffer to use to store the deleted keys |
| | | * @param entryID The ID of the entry that was modified. |
| | | * @param oldEntry The entry before the modifications were applied. |
| | | * @param newEntry The entry after the modifications were applied. |
| | | * @param mods The sequence of modifications in the Modify operation. |
| | | * @param options The indexing options to use |
| | | * @throws StorageRuntimeException If an error occurs in the JE database. |
| | | */ |
| | | public void modifyEntry(IndexBuffer buffer, |
| | | EntryID entryID, |
| | | Entry oldEntry, |
| | | Entry newEntry, |
| | | List<Modification> mods, IndexingOptions options) |
| | | throws StorageRuntimeException |
| | | { |
| | | TreeMap<ByteString, Boolean> modifiedKeys = |
| | | new TreeMap<ByteString, Boolean>(ByteSequence.COMPARATOR); |
| | | indexer.modifyEntry(oldEntry, newEntry, mods, modifiedKeys, options); |
| | | |
| | | for (Map.Entry<ByteString, Boolean> modifiedKey : modifiedKeys.entrySet()) |
| | | { |
| | | if(modifiedKey.getValue()) |
| | | { |
| | | insertID(buffer, modifiedKey.getKey(), entryID); |
| | | } |
| | | else |
| | | { |
| | | removeID(buffer, modifiedKey.getKey(), entryID); |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Set the index entry limit. |
| | | * |
| | | * @param indexEntryLimit The index entry limit to set. |
| | | * @return True if a rebuild is required or false otherwise. |
| | | */ |
| | | public boolean setIndexEntryLimit(int indexEntryLimit) |
| | | { |
| | | final boolean rebuildRequired = |
| | | this.indexEntryLimit < indexEntryLimit && entryLimitExceededCount > 0; |
| | | this.indexEntryLimit = indexEntryLimit; |
| | | return rebuildRequired; |
| | | } |
| | | |
| | | /** |
| | | * Set the indexer. |
| | | * |
| | | * @param indexer The indexer to set |
| | | */ |
| | | public void setIndexer(Indexer indexer) |
| | | { |
| | | this.indexer = indexer; |
| | | } |
| | | |
| | | /** |
| | | * Return entry limit. |
| | | * |
| | | * @return The entry limit. |
| | | */ |
| | | public int getIndexEntryLimit() { |
| | | return this.indexEntryLimit; |
| | | } |
| | | |
| | | /** |
| | | * Set the index trust state. |
| | | * @param txn A database transaction, or null if none is required. |
| | | * @param trusted True if this index should be trusted or false |
| | | * otherwise. |
| | | * @throws StorageRuntimeException If an error occurs in the JE database. |
| | | */ |
| | | public synchronized void setTrusted(WriteableStorage txn, boolean trusted) |
| | | throws StorageRuntimeException |
| | | { |
| | | this.trusted = trusted; |
| | | state.putIndexTrustState(txn, this, trusted); |
| | | } |
| | | |
| | | /** |
| | | * Return true iff this index is trusted. |
| | | * @return the trusted state of this index |
| | | */ |
| | | public synchronized boolean isTrusted() |
| | | { |
| | | return trusted; |
| | | } |
| | | |
| | | /** |
| | | * Return <code>true</code> iff this index is being rebuilt. |
| | | * @return The rebuild state of this index |
| | | */ |
| | | public synchronized boolean isRebuildRunning() |
| | | { |
| | | return rebuildRunning; |
| | | } |
| | | |
| | | /** |
| | | * Set the rebuild status of this index. |
| | | * @param rebuildRunning True if a rebuild process on this index |
| | | * is running or False otherwise. |
| | | */ |
| | | public synchronized void setRebuildStatus(boolean rebuildRunning) |
| | | { |
| | | this.rebuildRunning = rebuildRunning; |
| | | } |
| | | |
| | | /** |
| | | * Whether this index maintains a count of IDs for keys once the |
| | | * entry limit has exceeded. |
| | | * @return <code>true</code> if this index maintains court of IDs |
| | | * or <code>false</code> otherwise |
| | | */ |
| | | public boolean getMaintainCount() |
| | | { |
| | | return maintainCount; |
| | | } |
| | | } |
| New file |
| | |
| | | /* |
| | | * CDDL HEADER START |
| | | * |
| | | * The contents of this file are subject to the terms of the |
| | | * Common Development and Distribution License, Version 1.0 only |
| | | * (the "License"). You may not use this file except in compliance |
| | | * with the License. |
| | | * |
| | | * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt |
| | | * or http://forgerock.org/license/CDDLv1.0.html. |
| | | * See the License for the specific language governing permissions |
| | | * and limitations under the License. |
| | | * |
| | | * When distributing Covered Code, include this CDDL HEADER in each |
| | | * file and include the License file at legal-notices/CDDLv1_0.txt. |
| | | * If applicable, add the following below this CDDL HEADER, with the |
| | | * fields enclosed by brackets "[]" replaced with your own identifying |
| | | * information: |
| | | * Portions Copyright [yyyy] [name of copyright owner] |
| | | * |
| | | * CDDL HEADER END |
| | | * |
| | | * |
| | | * Copyright 2006-2008 Sun Microsystems, Inc. |
| | | * Portions Copyright 2014 ForgeRock AS |
| | | */ |
| | | package org.opends.server.backends.pluggable; |
| | | |
| | | import java.util.Iterator; |
| | | import java.util.LinkedHashMap; |
| | | import java.util.Map; |
| | | import java.util.TreeMap; |
| | | import java.util.TreeSet; |
| | | |
| | | import org.forgerock.opendj.ldap.ByteSequence; |
| | | import org.forgerock.opendj.ldap.ByteString; |
| | | import org.opends.server.backends.pluggable.BackendImpl.StorageRuntimeException; |
| | | import org.opends.server.backends.pluggable.BackendImpl.WriteableStorage; |
| | | import org.opends.server.types.DirectoryException; |
| | | |
| | | /** |
| | | * A buffered index is used to buffer multiple reads or writes to the |
| | | * same index key into a single read or write. |
| | | * It can only be used to buffer multiple reads and writes under |
| | | * the same transaction. The transaction may be null if it is known |
| | | * that there are no other concurrent updates to the index. |
| | | */ |
| | | public class IndexBuffer |
| | | { |
| | | private final EntryContainer entryContainer; |
| | | |
| | | /** |
| | | * The buffered records stored as a map from the record key to the |
| | | * buffered value for that key for each index. |
| | | */ |
| | | private final LinkedHashMap<Index, TreeMap<ByteString, BufferedIndexValues>> bufferedIndexes = |
| | | new LinkedHashMap<Index, TreeMap<ByteString, BufferedIndexValues>>(); |
| | | |
| | | /** The buffered records stored as a set of buffered VLV values for each index. */ |
| | | private final LinkedHashMap<VLVIndex, BufferedVLVValues> bufferedVLVIndexes = |
| | | new LinkedHashMap<VLVIndex, BufferedVLVValues>(); |
| | | |
| | | /** A simple class representing a pair of added and deleted indexed IDs. */ |
| | | static class BufferedIndexValues |
| | | { |
| | | private EntryIDSet addedIDs; |
| | | private EntryIDSet deletedIDs; |
| | | |
| | | /** |
| | | * Adds the provided entryID to this object associating it with the provided keyBytes. |
| | | * |
| | | * @param keyBytes the keyBytes mapping for this entryID |
| | | * @param entryID the entryID to add |
| | | */ |
| | | void addEntryID(ByteString keyBytes, EntryID entryID) |
| | | { |
| | | if (!remove(deletedIDs, entryID)) |
| | | { |
| | | if (this.addedIDs == null) |
| | | { |
| | | this.addedIDs = new EntryIDSet(keyBytes, null); |
| | | } |
| | | this.addedIDs.add(entryID); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Deletes the provided entryID from this object. |
| | | * |
| | | * @param keyBytes the keyBytes mapping for this entryID |
| | | * @param entryID the entryID to delete |
| | | */ |
| | | void deleteEntryID(ByteString keyBytes, EntryID entryID) |
| | | { |
| | | if (!remove(addedIDs, entryID)) |
| | | { |
| | | if (this.deletedIDs == null) |
| | | { |
| | | this.deletedIDs = new EntryIDSet(keyBytes, null); |
| | | } |
| | | this.deletedIDs.add(entryID); |
| | | } |
| | | } |
| | | |
| | | private boolean remove(EntryIDSet ids, EntryID entryID) |
| | | { |
| | | if (ids != null && ids.contains(entryID)) |
| | | { |
| | | ids.remove(entryID); |
| | | return true; |
| | | } |
| | | return false; |
| | | } |
| | | } |
| | | |
| | | /** A simple class representing a pair of added and deleted VLV values. */ |
| | | static class BufferedVLVValues |
| | | { |
| | | private TreeSet<SortValues> addedValues; |
| | | private TreeSet<SortValues> deletedValues; |
| | | |
| | | /** |
| | | * Adds the provided values to this object. |
| | | * |
| | | * @param sortValues the values to add |
| | | */ |
| | | void addValues(SortValues sortValues) |
| | | { |
| | | if (!remove(deletedValues, sortValues)) |
| | | { |
| | | if (this.addedValues == null) |
| | | { |
| | | this.addedValues = new TreeSet<SortValues>(); |
| | | } |
| | | this.addedValues.add(sortValues); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Deletes the provided values from this object. |
| | | * |
| | | * @param sortValues the values to delete |
| | | */ |
| | | void deleteValues(SortValues sortValues) |
| | | { |
| | | if (!remove(addedValues, sortValues)) |
| | | { |
| | | if (this.deletedValues == null) |
| | | { |
| | | this.deletedValues = new TreeSet<SortValues>(); |
| | | } |
| | | this.deletedValues.add(sortValues); |
| | | } |
| | | } |
| | | |
| | | private boolean remove(TreeSet<SortValues> values, SortValues sortValues) |
| | | { |
| | | if (values != null && values.contains(sortValues)) |
| | | { |
| | | values.remove(sortValues); |
| | | return true; |
| | | } |
| | | return false; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Construct a new empty index buffer object. |
| | | * |
| | | * @param entryContainer The database entryContainer using this |
| | | * index buffer. |
| | | */ |
| | | public IndexBuffer(EntryContainer entryContainer) |
| | | { |
| | | this.entryContainer = entryContainer; |
| | | } |
| | | |
| | | /** |
| | | * Get the buffered VLV values for the given VLV index. |
| | | * |
| | | * @param vlvIndex The VLV index with the buffered values to retrieve. |
| | | * @return The buffered VLV values or <code>null</code> if there are |
| | | * no buffered VLV values for the specified VLV index. |
| | | */ |
| | | public BufferedVLVValues getVLVIndex(VLVIndex vlvIndex) |
| | | { |
| | | BufferedVLVValues bufferedValues = bufferedVLVIndexes.get(vlvIndex); |
| | | if (bufferedValues == null) |
| | | { |
| | | bufferedValues = new BufferedVLVValues(); |
| | | bufferedVLVIndexes.put(vlvIndex, bufferedValues); |
| | | } |
| | | return bufferedValues; |
| | | } |
| | | |
| | | /** |
| | | * Get the buffered index values for the given index and keyBytes. |
| | | * |
| | | * @param index |
| | | * The index for which to retrieve the buffered index values |
| | | * @param keyBytes |
| | | * The keyBytes for which to retrieve the buffered index values |
| | | * @return The buffered index values, it can never be null |
| | | */ |
| | | BufferedIndexValues getBufferedIndexValues(Index index, ByteString keyBytes) |
| | | { |
| | | BufferedIndexValues values = null; |
| | | |
| | | TreeMap<ByteString, BufferedIndexValues> bufferedOperations = bufferedIndexes.get(index); |
| | | if (bufferedOperations == null) |
| | | { |
| | | bufferedOperations = new TreeMap<ByteString, BufferedIndexValues>(ByteSequence.COMPARATOR); |
| | | bufferedIndexes.put(index, bufferedOperations); |
| | | } |
| | | else |
| | | { |
| | | values = bufferedOperations.get(keyBytes); |
| | | } |
| | | |
| | | if (values == null) |
| | | { |
| | | values = new BufferedIndexValues(); |
| | | bufferedOperations.put(keyBytes, values); |
| | | } |
| | | return values; |
| | | } |
| | | |
| | | /** |
| | | * Flush the buffered index changes until the given transaction to |
| | | * the database. |
| | | * |
| | | * @param txn The database transaction to be used for the updates. |
| | | * @throws StorageRuntimeException If an error occurs in the JE database. |
| | | * @throws DirectoryException If a Directory Server error occurs. |
| | | */ |
| | | public void flush(WriteableStorage txn) throws StorageRuntimeException, DirectoryException |
| | | { |
| | | for (AttributeIndex attributeIndex : entryContainer.getAttributeIndexes()) |
| | | { |
| | | for (Index index : attributeIndex.getAllIndexes()) |
| | | { |
| | | updateKeys(index, txn, bufferedIndexes.remove(index)); |
| | | } |
| | | } |
| | | |
| | | for (VLVIndex vlvIndex : entryContainer.getVLVIndexes()) |
| | | { |
| | | BufferedVLVValues bufferedVLVValues = bufferedVLVIndexes.remove(vlvIndex); |
| | | if (bufferedVLVValues != null) |
| | | { |
| | | vlvIndex.updateIndex(txn, bufferedVLVValues.addedValues, bufferedVLVValues.deletedValues); |
| | | } |
| | | } |
| | | |
| | | final Index id2children = entryContainer.getID2Children(); |
| | | updateKeys(id2children, txn, bufferedIndexes.remove(id2children)); |
| | | |
| | | final Index id2subtree = entryContainer.getID2Subtree(); |
| | | final TreeMap<ByteString, BufferedIndexValues> bufferedValues = bufferedIndexes.remove(id2subtree); |
| | | if (bufferedValues != null) |
| | | { |
| | | /* |
| | | * OPENDJ-1375: add keys in reverse order to be consistent with single |
| | | * entry processing in add/delete processing. This is necessary in order |
| | | * to avoid deadlocks. |
| | | */ |
| | | updateKeys(id2subtree, txn, bufferedValues.descendingMap()); |
| | | } |
| | | } |
| | | |
| | | private void updateKeys(Index index, WriteableStorage txn, |
| | | Map<ByteString, BufferedIndexValues> bufferedValues) |
| | | { |
| | | if (bufferedValues != null) |
| | | { |
| | | final Iterator<Map.Entry<ByteString, BufferedIndexValues>> it = bufferedValues.entrySet().iterator(); |
| | | while (it.hasNext()) |
| | | { |
| | | final Map.Entry<ByteString, BufferedIndexValues> entry = it.next(); |
| | | final ByteString key = entry.getKey(); |
| | | final BufferedIndexValues values = entry.getValue(); |
| | | |
| | | index.updateKey(txn, key, values.deletedIDs, values.addedIDs); |
| | | |
| | | it.remove(); |
| | | } |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | /* |
| | | * CDDL HEADER START |
| | | * |
| | | * The contents of this file are subject to the terms of the |
| | | * Common Development and Distribution License, Version 1.0 only |
| | | * (the "License"). You may not use this file except in compliance |
| | | * with the License. |
| | | * |
| | | * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt |
| | | * or http://forgerock.org/license/CDDLv1.0.html. |
| | | * See the License for the specific language governing permissions |
| | | * and limitations under the License. |
| | | * |
| | | * When distributing Covered Code, include this CDDL HEADER in each |
| | | * file and include the License file at legal-notices/CDDLv1_0.txt. |
| | | * If applicable, add the following below this CDDL HEADER, with the |
| | | * fields enclosed by brackets "[]" replaced with your own identifying |
| | | * information: |
| | | * Portions Copyright [yyyy] [name of copyright owner] |
| | | * |
| | | * CDDL HEADER END |
| | | * |
| | | * |
| | | * Copyright 2006-2010 Sun Microsystems, Inc. |
| | | * Portions copyright 2011-2014 ForgeRock AS |
| | | * |
| | | */ |
| | | package org.opends.server.backends.pluggable; |
| | | |
| | | import java.util.ArrayList; |
| | | import java.util.HashMap; |
| | | import java.util.Map; |
| | | |
| | | import org.opends.server.backends.pluggable.AttributeIndex.IndexFilterType; |
| | | import org.opends.server.core.SearchOperation; |
| | | import org.opends.server.monitors.DatabaseEnvironmentMonitor; |
| | | import org.opends.server.types.AttributeType; |
| | | import org.opends.server.types.FilterType; |
| | | import org.opends.server.types.SearchFilter; |
| | | |
| | | import static org.opends.messages.JebMessages.*; |
| | | |
| | | /** |
| | | * An index filter is used to apply a search operation to a set of indexes |
| | | * to generate a set of candidate entries. |
| | | */ |
| | | public class IndexFilter |
| | | { |
| | | /** |
| | | * Stop processing the filter against the indexes when the |
| | | * number of candidates is smaller than this value. |
| | | */ |
| | | public static final int FILTER_CANDIDATE_THRESHOLD = 10; |
| | | |
| | | /** |
| | | * The entry entryContainer holding the attribute indexes. |
| | | */ |
| | | private final EntryContainer entryContainer; |
| | | |
| | | /** |
| | | * The search operation provides the search base, scope and filter. |
| | | * It can also be checked periodically for cancellation. |
| | | */ |
| | | private final SearchOperation searchOp; |
| | | |
| | | /** |
| | | * A string builder to hold a diagnostic string which helps determine |
| | | * how the indexed contributed to the search operation. |
| | | */ |
| | | private final StringBuilder buffer; |
| | | |
| | | private final DatabaseEnvironmentMonitor monitor; |
| | | |
| | | /** |
| | | * Construct an index filter for a search operation. |
| | | * |
| | | * @param entryContainer The entry entryContainer. |
| | | * @param searchOp The search operation to be evaluated. |
| | | * @param monitor The monitor to gather filter usage stats. |
| | | * |
| | | * @param debugBuilder If not null, a diagnostic string will be written |
| | | * which will help determine how the indexes contributed |
| | | * to this search. |
| | | */ |
| | | public IndexFilter(EntryContainer entryContainer, |
| | | SearchOperation searchOp, |
| | | StringBuilder debugBuilder, |
| | | DatabaseEnvironmentMonitor monitor) |
| | | { |
| | | this.entryContainer = entryContainer; |
| | | this.searchOp = searchOp; |
| | | this.buffer = debugBuilder; |
| | | this.monitor = monitor; |
| | | } |
| | | |
| | | /** |
| | | * Evaluate the search operation against the indexes. |
| | | * |
| | | * @return A set of entry IDs representing candidate entries. |
| | | */ |
| | | public EntryIDSet evaluate() |
| | | { |
| | | if (buffer != null) |
| | | { |
| | | buffer.append("filter="); |
| | | } |
| | | return evaluateFilter(searchOp.getFilter()); |
| | | } |
| | | |
| | | /** |
| | | * Evaluate a search filter against the indexes. |
| | | * |
| | | * @param filter The search filter to be evaluated. |
| | | * @return A set of entry IDs representing candidate entries. |
| | | */ |
| | | private EntryIDSet evaluateFilter(SearchFilter filter) |
| | | { |
| | | EntryIDSet candidates = evaluate(filter); |
| | | if (buffer != null) |
| | | { |
| | | candidates.toString(buffer); |
| | | } |
| | | return candidates; |
| | | } |
| | | |
| | | private EntryIDSet evaluate(SearchFilter filter) |
| | | { |
| | | switch (filter.getFilterType()) |
| | | { |
| | | case AND: |
| | | if (buffer != null) |
| | | { |
| | | buffer.append("(&"); |
| | | } |
| | | final EntryIDSet res1 = evaluateLogicalAndFilter(filter); |
| | | if (buffer != null) |
| | | { |
| | | buffer.append(")"); |
| | | } |
| | | return res1; |
| | | |
| | | case OR: |
| | | if (buffer != null) |
| | | { |
| | | buffer.append("(|"); |
| | | } |
| | | final EntryIDSet res2 = evaluateLogicalOrFilter(filter); |
| | | if (buffer != null) |
| | | { |
| | | buffer.append(")"); |
| | | } |
| | | return res2; |
| | | |
| | | case EQUALITY: |
| | | return evaluateFilterWithDiagnostic(IndexFilterType.EQUALITY, filter); |
| | | |
| | | case GREATER_OR_EQUAL: |
| | | return evaluateFilterWithDiagnostic(IndexFilterType.GREATER_OR_EQUAL, filter); |
| | | |
| | | case SUBSTRING: |
| | | return evaluateFilterWithDiagnostic(IndexFilterType.SUBSTRING, filter); |
| | | |
| | | case LESS_OR_EQUAL: |
| | | return evaluateFilterWithDiagnostic(IndexFilterType.LESS_OR_EQUAL, filter); |
| | | |
| | | case PRESENT: |
| | | return evaluateFilterWithDiagnostic(IndexFilterType.PRESENCE, filter); |
| | | |
| | | case APPROXIMATE_MATCH: |
| | | return evaluateFilterWithDiagnostic(IndexFilterType.APPROXIMATE, filter); |
| | | |
| | | case EXTENSIBLE_MATCH: |
| | | if (buffer!= null) |
| | | { |
| | | filter.toString(buffer); |
| | | } |
| | | return evaluateExtensibleFilter(filter); |
| | | |
| | | case NOT: |
| | | default: |
| | | if (buffer != null) |
| | | { |
| | | filter.toString(buffer); |
| | | } |
| | | //NYI |
| | | return new EntryIDSet(); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Evaluate a logical AND search filter against the indexes. |
| | | * |
| | | * @param andFilter The AND search filter to be evaluated. |
| | | * @return A set of entry IDs representing candidate entries. |
| | | */ |
| | | private EntryIDSet evaluateLogicalAndFilter(SearchFilter andFilter) |
| | | { |
| | | // Start off with an undefined set. |
| | | EntryIDSet results = new EntryIDSet(); |
| | | |
| | | // Put the slow range filters (greater-or-equal, less-or-equal) |
| | | // into a hash map, the faster components (equality, presence, approx) |
| | | // into one list and the remainder into another list. |
| | | |
| | | ArrayList<SearchFilter> fastComps = new ArrayList<SearchFilter>(); |
| | | ArrayList<SearchFilter> otherComps = new ArrayList<SearchFilter>(); |
| | | HashMap<AttributeType, ArrayList<SearchFilter>> rangeComps = |
| | | new HashMap<AttributeType, ArrayList<SearchFilter>>(); |
| | | |
| | | for (SearchFilter filter : andFilter.getFilterComponents()) |
| | | { |
| | | FilterType filterType = filter.getFilterType(); |
| | | if (filterType == FilterType.GREATER_OR_EQUAL || |
| | | filterType == FilterType.LESS_OR_EQUAL) |
| | | { |
| | | ArrayList<SearchFilter> rangeList; |
| | | rangeList = rangeComps.get(filter.getAttributeType()); |
| | | if (rangeList == null) |
| | | { |
| | | rangeList = new ArrayList<SearchFilter>(); |
| | | rangeComps.put(filter.getAttributeType(), rangeList); |
| | | } |
| | | rangeList.add(filter); |
| | | } |
| | | else if (filterType == FilterType.EQUALITY || |
| | | filterType == FilterType.PRESENT || |
| | | filterType == FilterType.APPROXIMATE_MATCH) |
| | | { |
| | | fastComps.add(filter); |
| | | } |
| | | else |
| | | { |
| | | otherComps.add(filter); |
| | | } |
| | | } |
| | | |
| | | // First, process the fast components. |
| | | if (evaluateFilters(results, fastComps) |
| | | // Next, process the other (non-range) components. |
| | | || evaluateFilters(results, otherComps) |
| | | // Are there any range component pairs like (cn>=A)(cn<=B) ? |
| | | || rangeComps.isEmpty()) |
| | | { |
| | | return results; |
| | | } |
| | | |
| | | // Next, process range component pairs like (cn>=A)(cn<=B). |
| | | ArrayList<SearchFilter> remainComps = new ArrayList<SearchFilter>(); |
| | | for (Map.Entry<AttributeType, ArrayList<SearchFilter>> rangeEntry : rangeComps.entrySet()) |
| | | { |
| | | ArrayList<SearchFilter> rangeList = rangeEntry.getValue(); |
| | | if (rangeList.size() == 2) |
| | | { |
| | | SearchFilter filter1 = rangeList.get(0); |
| | | SearchFilter filter2 = rangeList.get(1); |
| | | |
| | | AttributeIndex attributeIndex = entryContainer.getAttributeIndex(rangeEntry.getKey()); |
| | | if (attributeIndex == null) |
| | | { |
| | | if(monitor.isFilterUseEnabled()) |
| | | { |
| | | monitor.updateStats(SearchFilter.createANDFilter(rangeList), |
| | | INFO_JEB_INDEX_FILTER_INDEX_TYPE_DISABLED.get("ordering", |
| | | rangeEntry.getKey().getNameOrOID())); |
| | | } |
| | | continue; |
| | | } |
| | | |
| | | EntryIDSet set = attributeIndex.evaluateBoundedRange(filter1, filter2, buffer, monitor); |
| | | if(monitor.isFilterUseEnabled() && set.isDefined()) |
| | | { |
| | | monitor.updateStats(SearchFilter.createANDFilter(rangeList), set.size()); |
| | | } |
| | | if (retainAll(results, set)) |
| | | { |
| | | return results; |
| | | } |
| | | } |
| | | else |
| | | { |
| | | // Add to the remaining range components to be processed. |
| | | remainComps.addAll(rangeList); |
| | | } |
| | | } |
| | | |
| | | // Finally, process the remaining slow range components. |
| | | evaluateFilters(results, remainComps); |
| | | |
| | | return results; |
| | | } |
| | | |
| | | private boolean evaluateFilters(EntryIDSet results, ArrayList<SearchFilter> filters) |
| | | { |
| | | for (SearchFilter filter : filters) |
| | | { |
| | | final EntryIDSet filteredSet = evaluateFilter(filter); |
| | | if (retainAll(results, filteredSet)) |
| | | { |
| | | return true; |
| | | } |
| | | } |
| | | return false; |
| | | } |
| | | |
| | | /** |
| | | * Retain all IDs in a given set that appear in a second set. |
| | | * |
| | | * @param a The set of entry IDs to be updated. |
| | | * @param b Only those IDs that are in this set are retained. |
| | | * @return true if the number of IDs in the updated set is now below |
| | | * the filter candidate threshold. |
| | | */ |
| | | private boolean retainAll(EntryIDSet a, EntryIDSet b) |
| | | { |
| | | a.retainAll(b); |
| | | |
| | | // We may have reached the point of diminishing returns where |
| | | // it is quicker to stop now and process the current small number of candidates. |
| | | return a.isDefined() && a.size() <= FILTER_CANDIDATE_THRESHOLD; |
| | | } |
| | | |
| | | /** |
| | | * Evaluate a logical OR search filter against the indexes. |
| | | * |
| | | * @param orFilter The OR search filter to be evaluated. |
| | | * @return A set of entry IDs representing candidate entries. |
| | | */ |
| | | private EntryIDSet evaluateLogicalOrFilter(SearchFilter orFilter) |
| | | { |
| | | ArrayList<EntryIDSet> candidateSets = new ArrayList<EntryIDSet>( |
| | | orFilter.getFilterComponents().size()); |
| | | |
| | | for (SearchFilter filter : orFilter.getFilterComponents()) |
| | | { |
| | | EntryIDSet set = evaluateFilter(filter); |
| | | if (!set.isDefined()) |
| | | { |
| | | // There is no point continuing. |
| | | return set; |
| | | } |
| | | candidateSets.add(set); |
| | | } |
| | | return EntryIDSet.unionOfSets(candidateSets, false); |
| | | } |
| | | |
| | | private EntryIDSet evaluateFilterWithDiagnostic(IndexFilterType indexFilterType, SearchFilter filter) |
| | | { |
| | | if (buffer != null) |
| | | { |
| | | filter.toString(buffer); |
| | | } |
| | | return evaluateFilter(indexFilterType, filter); |
| | | } |
| | | |
| | | private EntryIDSet evaluateFilter(IndexFilterType indexFilterType, SearchFilter filter) |
| | | { |
| | | AttributeIndex attributeIndex = entryContainer.getAttributeIndex(filter.getAttributeType()); |
| | | if (attributeIndex != null) |
| | | { |
| | | return attributeIndex.evaluateFilter(indexFilterType, filter, buffer, monitor); |
| | | } |
| | | |
| | | if (monitor.isFilterUseEnabled()) |
| | | { |
| | | monitor.updateStats(filter, INFO_JEB_INDEX_FILTER_INDEX_TYPE_DISABLED.get( |
| | | indexFilterType.toString(), filter.getAttributeType().getNameOrOID())); |
| | | } |
| | | return new EntryIDSet(); |
| | | } |
| | | |
| | | /** |
| | | * Evaluate an extensible filter against the indexes. |
| | | * |
| | | * @param extensibleFilter The extensible filter to be evaluated. |
| | | * @return A set of entry IDs representing candidate entries. |
| | | */ |
| | | private EntryIDSet evaluateExtensibleFilter(SearchFilter extensibleFilter) |
| | | { |
| | | if (extensibleFilter.getDNAttributes()) |
| | | { |
| | | // This will always be unindexed since the filter potentially matches |
| | | // entries containing the specified attribute type as well as any entry |
| | | // containing the attribute in its DN as part of a superior RDN. |
| | | return IndexQuery.createNullIndexQuery().evaluate(null); |
| | | } |
| | | |
| | | AttributeIndex attributeIndex = entryContainer.getAttributeIndex(extensibleFilter.getAttributeType()); |
| | | if (attributeIndex != null) |
| | | { |
| | | return attributeIndex.evaluateExtensibleFilter(extensibleFilter, buffer, monitor); |
| | | } |
| | | return IndexQuery.createNullIndexQuery().evaluate(null); |
| | | } |
| | | } |
| New file |
| | |
| | | /* |
| | | * CDDL HEADER START |
| | | * |
| | | * The contents of this file are subject to the terms of the |
| | | * Common Development and Distribution License, Version 1.0 only |
| | | * (the "License"). You may not use this file except in compliance |
| | | * with the License. |
| | | * |
| | | * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt |
| | | * or http://forgerock.org/license/CDDLv1.0.html. |
| | | * See the License for the specific language governing permissions |
| | | * and limitations under the License. |
| | | * |
| | | * When distributing Covered Code, include this CDDL HEADER in each |
| | | * file and include the License file at legal-notices/CDDLv1_0.txt. |
| | | * If applicable, add the following below this CDDL HEADER, with the |
| | | * fields enclosed by brackets "[]" replaced with your own identifying |
| | | * information: |
| | | * Portions Copyright [yyyy] [name of copyright owner] |
| | | * |
| | | * CDDL HEADER END |
| | | * |
| | | * |
| | | * Copyright 2009-2010 Sun Microsystems, Inc. |
| | | * Portions Copyright 2014 ForgeRock AS |
| | | */ |
| | | package org.opends.server.backends.pluggable; |
| | | |
| | | import java.util.Collection; |
| | | |
| | | import org.forgerock.i18n.LocalizableMessageBuilder; |
| | | |
| | | import static org.opends.server.backends.jeb.IndexFilter.*; |
| | | |
| | | /** |
| | | * This class represents a JE Backend Query. |
| | | */ |
| | | @org.opends.server.types.PublicAPI( |
| | | stability = org.opends.server.types.StabilityLevel.VOLATILE, |
| | | mayInstantiate = false, |
| | | mayExtend = true, |
| | | mayInvoke = false) |
| | | public abstract class IndexQuery |
| | | { |
| | | /** |
| | | * Evaluates the index query and returns the EntryIDSet. |
| | | * |
| | | * @param debugMessage If not null, diagnostic message will be written |
| | | * which will help to determine why the returned |
| | | * EntryIDSet is not defined. |
| | | * @return The EntryIDSet as a result of evaluation of this query. |
| | | */ |
| | | public abstract EntryIDSet evaluate(LocalizableMessageBuilder debugMessage); |
| | | |
| | | |
| | | |
| | | /** |
| | | * Creates an IntersectionIndexQuery object from a collection of |
| | | * IndexQuery objects. |
| | | * |
| | | * @param subIndexQueries |
| | | * A collection of IndexQuery objects. |
| | | * @return An IntersectionIndexQuery object. |
| | | */ |
| | | public static IndexQuery createIntersectionIndexQuery( |
| | | Collection<IndexQuery> subIndexQueries) |
| | | { |
| | | return new IntersectionIndexQuery(subIndexQueries); |
| | | } |
| | | |
| | | |
| | | |
| | | /** |
| | | * Creates a union IndexQuery object from a collection of IndexQuery |
| | | * objects. |
| | | * |
| | | * @param subIndexQueries |
| | | * Collection of IndexQuery objects. |
| | | * @return A UnionIndexQuery object. |
| | | */ |
| | | public static IndexQuery createUnionIndexQuery( |
| | | Collection<IndexQuery> subIndexQueries) |
| | | { |
| | | return new UnionIndexQuery(subIndexQueries); |
| | | } |
| | | |
| | | |
| | | |
| | | /** |
| | | * Creates an empty IndexQuery object. |
| | | * |
| | | * @return A NullIndexQuery object. |
| | | */ |
| | | public static IndexQuery createNullIndexQuery() |
| | | { |
| | | return new NullIndexQuery(); |
| | | } |
| | | |
| | | |
| | | |
| | | /** |
| | | * This class creates a Null IndexQuery. It is used when there is no |
| | | * record in the index. It may also be used when the index contains |
| | | * all the records but an empty EntryIDSet should be returned as part |
| | | * of the optimization. |
| | | */ |
| | | private static final class NullIndexQuery extends IndexQuery |
| | | { |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public EntryIDSet evaluate(LocalizableMessageBuilder debugMessage) |
| | | { |
| | | return new EntryIDSet(); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * This class creates an intersection IndexQuery from a collection of |
| | | * IndexQuery objects. |
| | | */ |
| | | private static final class IntersectionIndexQuery extends IndexQuery |
| | | { |
| | | /** |
| | | * Collection of IndexQuery objects. |
| | | */ |
| | | private final Collection<IndexQuery> subIndexQueries; |
| | | |
| | | |
| | | |
| | | /** |
| | | * Creates an instance of IntersectionIndexQuery. |
| | | * |
| | | * @param subIndexQueries |
| | | * Collection of IndexQuery objects. |
| | | */ |
| | | private IntersectionIndexQuery(Collection<IndexQuery> subIndexQueries) |
| | | { |
| | | this.subIndexQueries = subIndexQueries; |
| | | } |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public EntryIDSet evaluate(LocalizableMessageBuilder debugMessage) |
| | | { |
| | | EntryIDSet entryIDs = null; |
| | | for (IndexQuery query : subIndexQueries) |
| | | { |
| | | if (entryIDs == null) |
| | | { |
| | | entryIDs = query.evaluate(debugMessage); |
| | | } |
| | | else |
| | | { |
| | | entryIDs.retainAll(query.evaluate(debugMessage)); |
| | | } |
| | | if (entryIDs.isDefined() |
| | | && entryIDs.size() <= FILTER_CANDIDATE_THRESHOLD) |
| | | { |
| | | break; |
| | | } |
| | | } |
| | | return entryIDs; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * This class creates a union of IndexQuery objects. |
| | | */ |
| | | private static final class UnionIndexQuery extends IndexQuery |
| | | { |
| | | /** |
| | | * Collection containing IndexQuery objects. |
| | | */ |
| | | private final Collection<IndexQuery> subIndexQueries; |
| | | |
| | | |
| | | |
| | | /** |
| | | * Creates an instance of UnionIndexQuery. |
| | | * |
| | | * @param subIndexQueries |
| | | * The Collection of IndexQuery objects. |
| | | */ |
| | | private UnionIndexQuery(Collection<IndexQuery> subIndexQueries) |
| | | { |
| | | this.subIndexQueries = subIndexQueries; |
| | | } |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public EntryIDSet evaluate(LocalizableMessageBuilder debugMessage) |
| | | { |
| | | EntryIDSet entryIDs = null; |
| | | for (IndexQuery query : subIndexQueries) |
| | | { |
| | | if (entryIDs == null) |
| | | { |
| | | entryIDs = query.evaluate(debugMessage); |
| | | } |
| | | else |
| | | { |
| | | entryIDs.addAll(query.evaluate(debugMessage)); |
| | | } |
| | | if (entryIDs.isDefined() |
| | | && entryIDs.size() <= FILTER_CANDIDATE_THRESHOLD) |
| | | { |
| | | break; |
| | | } |
| | | } |
| | | return entryIDs; |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | /* |
| | | * CDDL HEADER START |
| | | * |
| | | * The contents of this file are subject to the terms of the |
| | | * Common Development and Distribution License, Version 1.0 only |
| | | * (the "License"). You may not use this file except in compliance |
| | | * with the License. |
| | | * |
| | | * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt |
| | | * or http://forgerock.org/license/CDDLv1.0.html. |
| | | * See the License for the specific language governing permissions |
| | | * and limitations under the License. |
| | | * |
| | | * When distributing Covered Code, include this CDDL HEADER in each |
| | | * file and include the License file at legal-notices/CDDLv1_0.txt. |
| | | * If applicable, add the following below this CDDL HEADER, with the |
| | | * fields enclosed by brackets "[]" replaced with your own identifying |
| | | * information: |
| | | * Portions Copyright [yyyy] [name of copyright owner] |
| | | * |
| | | * CDDL HEADER END |
| | | * |
| | | * |
| | | * Copyright 2009-2010 Sun Microsystems, Inc. |
| | | * Portions Copyright 2014 ForgeRock AS |
| | | */ |
| | | package org.opends.server.backends.pluggable; |
| | | |
| | | import java.util.Collection; |
| | | import java.util.Map; |
| | | |
| | | import org.forgerock.i18n.LocalizableMessageBuilder; |
| | | import org.forgerock.opendj.ldap.ByteSequence; |
| | | import org.forgerock.opendj.ldap.spi.IndexQueryFactory; |
| | | import org.forgerock.opendj.ldap.spi.IndexingOptions; |
| | | |
| | | import static org.opends.messages.JebMessages.*; |
| | | |
| | | /** |
| | | * This class is an implementation of IndexQueryFactory which creates |
| | | * IndexQuery objects as part of the query of the JEB index. |
| | | */ |
| | | public final class IndexQueryFactoryImpl implements |
| | | IndexQueryFactory<IndexQuery> |
| | | { |
| | | |
| | | private static final String PRESENCE_INDEX_KEY = "presence"; |
| | | |
| | | /** |
| | | * The Map containing the string type identifier and the corresponding index. |
| | | */ |
| | | private final Map<String, Index> indexMap; |
| | | private final IndexingOptions indexingOptions; |
| | | |
| | | /** |
| | | * Creates a new IndexQueryFactoryImpl object. |
| | | * |
| | | * @param indexMap |
| | | * A map containing the index id and the corresponding index. |
| | | * @param indexingOptions |
| | | * The options to use for indexing |
| | | */ |
| | | public IndexQueryFactoryImpl(Map<String, Index> indexMap, IndexingOptions indexingOptions) |
| | | { |
| | | this.indexMap = indexMap; |
| | | this.indexingOptions = indexingOptions; |
| | | } |
| | | |
| | | |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public IndexQuery createExactMatchQuery(final String indexID, final ByteSequence key) |
| | | { |
| | | return new IndexQuery() |
| | | { |
| | | |
| | | @Override |
| | | public EntryIDSet evaluate(LocalizableMessageBuilder debugMessage) |
| | | { |
| | | // Read the database and get Record for the key. |
| | | // Select the right index to be used. |
| | | Index index = indexMap.get(indexID); |
| | | if (index == null) |
| | | { |
| | | if(debugMessage != null) |
| | | { |
| | | debugMessage.append(INFO_JEB_INDEX_FILTER_INDEX_TYPE_DISABLED.get(indexID, "")); |
| | | } |
| | | return createMatchAllQuery().evaluate(debugMessage); |
| | | } |
| | | EntryIDSet entrySet = index.readKey(key, null); |
| | | if(debugMessage != null && !entrySet.isDefined()) |
| | | { |
| | | updateStatsUndefinedResults(debugMessage, index); |
| | | } |
| | | return entrySet; |
| | | } |
| | | }; |
| | | } |
| | | |
| | | |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public IndexQuery createRangeMatchQuery(final String indexID, |
| | | final ByteSequence lowerBound, final ByteSequence upperBound, |
| | | final boolean includeLowerBound, final boolean includeUpperBound) |
| | | { |
| | | return new IndexQuery() |
| | | { |
| | | |
| | | @Override |
| | | public EntryIDSet evaluate(LocalizableMessageBuilder debugMessage) |
| | | { |
| | | // Find the right index. |
| | | Index index = indexMap.get(indexID); |
| | | if (index == null) |
| | | { |
| | | if(debugMessage != null) |
| | | { |
| | | debugMessage.append(INFO_JEB_INDEX_FILTER_INDEX_TYPE_DISABLED.get(indexID, "")); |
| | | } |
| | | return createMatchAllQuery().evaluate(debugMessage); |
| | | } |
| | | EntryIDSet entrySet = index.readRange(lowerBound, upperBound, |
| | | includeLowerBound, includeUpperBound); |
| | | if(debugMessage != null && !entrySet.isDefined()) |
| | | { |
| | | updateStatsUndefinedResults(debugMessage, index); |
| | | } |
| | | return entrySet; |
| | | } |
| | | }; |
| | | } |
| | | |
| | | |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public IndexQuery createIntersectionQuery(Collection<IndexQuery> subqueries) |
| | | { |
| | | return IndexQuery.createIntersectionIndexQuery(subqueries); |
| | | } |
| | | |
| | | |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public IndexQuery createUnionQuery(Collection<IndexQuery> subqueries) |
| | | { |
| | | return IndexQuery.createUnionIndexQuery(subqueries); |
| | | } |
| | | |
| | | |
| | | |
| | | /** |
| | | * {@inheritDoc} |
| | | * <p> |
| | | * It returns an empty EntryIDSet object when either all or no record |
| | | * sets are requested. |
| | | */ |
| | | @Override |
| | | public IndexQuery createMatchAllQuery() |
| | | { |
| | | return new IndexQuery() |
| | | { |
| | | @Override |
| | | public EntryIDSet evaluate(LocalizableMessageBuilder debugMessage) |
| | | { |
| | | final String indexID = PRESENCE_INDEX_KEY; |
| | | final Index index = indexMap.get(indexID); |
| | | if (index == null) |
| | | { |
| | | if(debugMessage != null) |
| | | { |
| | | debugMessage.append(INFO_JEB_INDEX_FILTER_INDEX_TYPE_DISABLED.get(indexID, "")); |
| | | } |
| | | return new EntryIDSet(); |
| | | } |
| | | |
| | | EntryIDSet entrySet = index.readKey(PresenceIndexer.presenceKey, null); |
| | | if (debugMessage != null && !entrySet.isDefined()) |
| | | { |
| | | updateStatsUndefinedResults(debugMessage, index); |
| | | } |
| | | return entrySet; |
| | | } |
| | | }; |
| | | } |
| | | |
| | | private static void updateStatsUndefinedResults(LocalizableMessageBuilder debugMessage, Index index) |
| | | { |
| | | if (!index.isTrusted()) |
| | | { |
| | | debugMessage.append(INFO_JEB_INDEX_FILTER_INDEX_NOT_TRUSTED.get(index.getName())); |
| | | } |
| | | else if (index.isRebuildRunning()) |
| | | { |
| | | debugMessage.append(INFO_JEB_INDEX_FILTER_INDEX_REBUILD_IN_PROGRESS.get(index.getName())); |
| | | } |
| | | else |
| | | { |
| | | debugMessage.append(INFO_JEB_INDEX_FILTER_INDEX_LIMIT_EXCEEDED.get(index.getName())); |
| | | } |
| | | } |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public IndexingOptions getIndexingOptions() |
| | | { |
| | | return indexingOptions; |
| | | } |
| | | } |
| New file |
| | |
| | | /* |
| | | * CDDL HEADER START |
| | | * |
| | | * The contents of this file are subject to the terms of the |
| | | * Common Development and Distribution License, Version 1.0 only |
| | | * (the "License"). You may not use this file except in compliance |
| | | * with the License. |
| | | * |
| | | * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt |
| | | * or http://forgerock.org/license/CDDLv1.0.html. |
| | | * See the License for the specific language governing permissions |
| | | * and limitations under the License. |
| | | * |
| | | * When distributing Covered Code, include this CDDL HEADER in each |
| | | * file and include the License file at legal-notices/CDDLv1_0.txt. |
| | | * If applicable, add the following below this CDDL HEADER, with the |
| | | * fields enclosed by brackets "[]" replaced with your own identifying |
| | | * information: |
| | | * Portions Copyright [yyyy] [name of copyright owner] |
| | | * |
| | | * CDDL HEADER END |
| | | * |
| | | * |
| | | * Copyright 2006-2008 Sun Microsystems, Inc. |
| | | * Portions copyright 2012-2014 ForgeRock AS. |
| | | */ |
| | | package org.opends.server.backends.pluggable; |
| | | |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import java.util.Set; |
| | | |
| | | import org.forgerock.opendj.ldap.ByteString; |
| | | import org.forgerock.opendj.ldap.spi.IndexingOptions; |
| | | import org.opends.server.types.Entry; |
| | | import org.opends.server.types.Modification; |
| | | |
| | | /** |
| | | * This class attempts to abstract the generation and comparison of keys |
| | | * for an index. It is subclassed for the specific type of indexing. |
| | | */ |
| | | public abstract class Indexer |
| | | { |
| | | /** |
| | | * Generate the set of index keys for an entry. |
| | | * |
| | | * @param entry The entry. |
| | | * @param keys The set into which the generated keys will be inserted. |
| | | * @param options The indexing options to use |
| | | */ |
| | | public abstract void indexEntry(Entry entry, Set<ByteString> keys, IndexingOptions options); |
| | | |
| | | /** |
| | | * Generate the set of index keys to be added and the set of index keys |
| | | * to be deleted for an entry that has been replaced. |
| | | * |
| | | * @param oldEntry The original entry contents. |
| | | * @param newEntry The new entry contents. |
| | | * @param modifiedKeys The map into which the modified keys will be inserted. |
| | | * @param options The indexing options to use |
| | | */ |
| | | public abstract void replaceEntry(Entry oldEntry, Entry newEntry, |
| | | Map<ByteString, Boolean> modifiedKeys, IndexingOptions options); |
| | | |
| | | /** |
| | | * Generate the set of index keys to be added and the set of index keys |
| | | * to be deleted for an entry that was modified. |
| | | * |
| | | * @param oldEntry The original entry contents. |
| | | * @param newEntry The new entry contents. |
| | | * @param mods The set of modifications that were applied to the entry. |
| | | * @param modifiedKeys The map into which the modified keys will be inserted. |
| | | * @param options The indexing options to use |
| | | */ |
| | | public abstract void modifyEntry(Entry oldEntry, Entry newEntry, |
| | | List<Modification> mods, Map<ByteString, Boolean> modifiedKeys, |
| | | IndexingOptions options); |
| | | |
| | | /** |
| | | * Get a string representation of this object. The returned value is |
| | | * used to name an index created using this object. |
| | | * @return A string representation of this object. |
| | | */ |
| | | @Override |
| | | public abstract String toString(); |
| | | } |
| New file |
| | |
| | | /* |
| | | * CDDL HEADER START |
| | | * |
| | | * The contents of this file are subject to the terms of the |
| | | * Common Development and Distribution License, Version 1.0 only |
| | | * (the "License"). You may not use this file except in compliance |
| | | * with the License. |
| | | * |
| | | * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt |
| | | * or http://forgerock.org/license/CDDLv1.0.html. |
| | | * See the License for the specific language governing permissions |
| | | * and limitations under the License. |
| | | * |
| | | * When distributing Covered Code, include this CDDL HEADER in each |
| | | * file and include the License file at legal-notices/CDDLv1_0.txt. |
| | | * If applicable, add the following below this CDDL HEADER, with the |
| | | * fields enclosed by brackets "[]" replaced with your own identifying |
| | | * information: |
| | | * Portions Copyright [yyyy] [name of copyright owner] |
| | | * |
| | | * CDDL HEADER END |
| | | * |
| | | * |
| | | * Copyright 2008-2009 Sun Microsystems, Inc. |
| | | * Portions Copyright 2013-2014 ForgeRock AS. |
| | | */ |
| | | package org.opends.server.backends.pluggable; |
| | | |
| | | import java.io.IOException; |
| | | import java.util.Collection; |
| | | import java.util.LinkedList; |
| | | import java.util.List; |
| | | |
| | | import org.forgerock.i18n.LocalizableMessage; |
| | | import org.forgerock.i18n.slf4j.LocalizedLogger; |
| | | import org.forgerock.opendj.io.ASN1; |
| | | import org.forgerock.opendj.io.ASN1Reader; |
| | | import org.forgerock.opendj.io.ASN1Writer; |
| | | import org.forgerock.opendj.ldap.ByteStringBuilder; |
| | | import org.opends.server.api.CompressedSchema; |
| | | import org.opends.server.backends.pluggable.BackendImpl.Cursor; |
| | | import org.opends.server.backends.pluggable.BackendImpl.Storage; |
| | | import org.opends.server.backends.pluggable.BackendImpl.StorageRuntimeException; |
| | | import org.opends.server.core.DirectoryServer; |
| | | import org.opends.server.types.DirectoryException; |
| | | import org.opends.server.types.InitializationException; |
| | | import org.opends.server.util.StaticUtils; |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | import static com.sleepycat.je.LockMode.*; |
| | | import static com.sleepycat.je.OperationStatus.*; |
| | | |
| | | import static org.opends.messages.JebMessages.*; |
| | | |
| | | /** |
| | | * This class provides a compressed schema implementation whose definitions are |
| | | * stored in a Berkeley DB JE database. |
| | | */ |
| | | public final class JECompressedSchema extends CompressedSchema |
| | | { |
| | | private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); |
| | | |
| | | /** The name of the database used to store compressed attribute description definitions. */ |
| | | private static final String DB_NAME_AD = "compressed_attributes"; |
| | | /** The name of the database used to store compressed object class set definitions. */ |
| | | private static final String DB_NAME_OC = "compressed_object_classes"; |
| | | |
| | | /** The compressed attribute description schema database. */ |
| | | private Database adDatabase; |
| | | /** The environment in which the databases are held. */ |
| | | private Storage environment; |
| | | /** The compressed object class set schema database. */ |
| | | private Database ocDatabase; |
| | | |
| | | private final ByteStringBuilder storeAttributeWriterBuffer = new ByteStringBuilder(); |
| | | private final ASN1Writer storeAttributeWriter = ASN1.getWriter(storeAttributeWriterBuffer); |
| | | private final ByteStringBuilder storeObjectClassesWriterBuffer = new ByteStringBuilder(); |
| | | private final ASN1Writer storeObjectClassesWriter = ASN1.getWriter(storeObjectClassesWriterBuffer); |
| | | |
| | | |
| | | |
| | | /** |
| | | * Creates a new instance of this JE compressed schema manager. |
| | | * |
| | | * @param environment |
| | | * A reference to the database environment in which the databases |
| | | * will be held. |
| | | * @throws StorageRuntimeException |
| | | * If a database problem occurs while loading the compressed schema |
| | | * definitions from the database. |
| | | * @throws InitializationException |
| | | * If an error occurs while loading and processing the compressed |
| | | * schema definitions. |
| | | */ |
| | | public JECompressedSchema(final Storage environment) |
| | | throws StorageRuntimeException, InitializationException |
| | | { |
| | | this.environment = environment; |
| | | load(); |
| | | } |
| | | |
| | | |
| | | |
| | | /** |
| | | * Closes the databases and releases any resources held by this compressed |
| | | * schema manager. |
| | | */ |
| | | public void close() |
| | | { |
| | | close0(adDatabase); |
| | | close0(ocDatabase); |
| | | |
| | | adDatabase = null; |
| | | ocDatabase = null; |
| | | environment = null; |
| | | } |
| | | |
| | | private void close0(Database database) |
| | | { |
| | | try |
| | | { |
| | | database.sync(); |
| | | } |
| | | catch (final Exception e) |
| | | { |
| | | // Ignore. |
| | | } |
| | | StaticUtils.close(database); |
| | | } |
| | | |
| | | |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | protected void storeAttribute(final byte[] encodedAttribute, |
| | | final String attributeName, final Collection<String> attributeOptions) |
| | | throws DirectoryException |
| | | { |
| | | try |
| | | { |
| | | storeAttributeWriterBuffer.clear(); |
| | | storeAttributeWriter.writeStartSequence(); |
| | | storeAttributeWriter.writeOctetString(attributeName); |
| | | for (final String option : attributeOptions) |
| | | { |
| | | storeAttributeWriter.writeOctetString(option); |
| | | } |
| | | storeAttributeWriter.writeEndSequence(); |
| | | store(adDatabase, encodedAttribute, storeAttributeWriterBuffer); |
| | | } |
| | | catch (final IOException e) |
| | | { |
| | | // TODO: Shouldn't happen but should log a message |
| | | } |
| | | } |
| | | |
| | | |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | protected void storeObjectClasses(final byte[] encodedObjectClasses, |
| | | final Collection<String> objectClassNames) throws DirectoryException |
| | | { |
| | | try |
| | | { |
| | | storeObjectClassesWriterBuffer.clear(); |
| | | storeObjectClassesWriter.writeStartSequence(); |
| | | for (final String ocName : objectClassNames) |
| | | { |
| | | storeObjectClassesWriter.writeOctetString(ocName); |
| | | } |
| | | storeObjectClassesWriter.writeEndSequence(); |
| | | store(ocDatabase, encodedObjectClasses, storeObjectClassesWriterBuffer); |
| | | } |
| | | catch (final IOException e) |
| | | { |
| | | // TODO: Shouldn't happen but should log a message |
| | | } |
| | | } |
| | | |
| | | |
| | | |
| | | /** |
| | | * Loads the compressed schema information from the database. |
| | | * |
| | | * @throws StorageRuntimeException |
| | | * If a database error occurs while loading the definitions from the |
| | | * database. |
| | | * @throws InitializationException |
| | | * If an error occurs while loading and processing the definitions. |
| | | */ |
| | | private void load() throws StorageRuntimeException, InitializationException |
| | | { |
| | | final DatabaseConfig dbConfig = JEBUtils.toDatabaseConfigNoDuplicates(environment); |
| | | |
| | | adDatabase = environment.openDatabase(null, DB_NAME_AD, dbConfig); |
| | | ocDatabase = environment.openDatabase(null, DB_NAME_OC, dbConfig); |
| | | |
| | | // Cursor through the object class database and load the object class set |
| | | // definitions. At the same time, figure out the highest token value and |
| | | // initialize the object class counter to one greater than that. |
| | | final Cursor ocCursor = ocDatabase.openCursor(null); |
| | | try |
| | | { |
| | | while (ocCursor.next()) |
| | | { |
| | | final byte[] encodedObjectClasses = ocCursor.getKey().toByteArray(); |
| | | final ASN1Reader reader = ASN1.getReader(ocCursor.getValue()); |
| | | reader.readStartSequence(); |
| | | final List<String> objectClassNames = new LinkedList<String>(); |
| | | while (reader.hasNextElement()) |
| | | { |
| | | objectClassNames.add(reader.readOctetStringAsString()); |
| | | } |
| | | reader.readEndSequence(); |
| | | loadObjectClasses(encodedObjectClasses, objectClassNames); |
| | | } |
| | | } |
| | | catch (final IOException e) |
| | | { |
| | | logger.traceException(e); |
| | | throw new InitializationException( |
| | | ERR_JEB_COMPSCHEMA_CANNOT_DECODE_OC_TOKEN.get(e.getMessage()), e); |
| | | } |
| | | finally |
| | | { |
| | | ocCursor.close(); |
| | | } |
| | | |
| | | // Cursor through the attribute description database and load the attribute |
| | | // set definitions. |
| | | final Cursor adCursor = adDatabase.openCursor(null); |
| | | try |
| | | { |
| | | while (adCursor.next()) |
| | | { |
| | | final byte[] encodedAttribute = adCursor.getKey().toByteArray(); |
| | | final ASN1Reader reader = ASN1.getReader(adCursor.getValue()); |
| | | reader.readStartSequence(); |
| | | final String attributeName = reader.readOctetStringAsString(); |
| | | final List<String> attributeOptions = new LinkedList<String>(); |
| | | while (reader.hasNextElement()) |
| | | { |
| | | attributeOptions.add(reader.readOctetStringAsString()); |
| | | } |
| | | reader.readEndSequence(); |
| | | loadAttribute(encodedAttribute, attributeName, attributeOptions); |
| | | } |
| | | } |
| | | catch (final IOException e) |
| | | { |
| | | logger.traceException(e); |
| | | throw new InitializationException( |
| | | ERR_JEB_COMPSCHEMA_CANNOT_DECODE_AD_TOKEN.get(e.getMessage()), e); |
| | | } |
| | | finally |
| | | { |
| | | adCursor.close(); |
| | | } |
| | | } |
| | | |
| | | |
| | | |
| | | private void store(final Database database, final byte[] key, final ByteStringBuilder value) throws DirectoryException |
| | | { |
| | | if (!putNoOverwrite(database, key, value)) |
| | | { |
| | | final LocalizableMessage m = ERR_JEB_COMPSCHEMA_CANNOT_STORE_MULTIPLE_FAILURES.get(); |
| | | throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), m); |
| | | } |
| | | } |
| | | |
| | | private boolean putNoOverwrite(final Database database, final byte[] key, final ByteStringBuilder value) |
| | | throws DirectoryException |
| | | { |
| | | final ByteString keyEntry = new ByteString(key); |
| | | final ByteString valueEntry = new ByteString(value.getBackingArray(), 0, value.length()); |
| | | for (int i = 0; i < 3; i++) |
| | | { |
| | | try |
| | | { |
| | | final OperationStatus status = database.putNoOverwrite(null, keyEntry, valueEntry); |
| | | if (status != SUCCESS) |
| | | { |
| | | final LocalizableMessage m = ERR_JEB_COMPSCHEMA_CANNOT_STORE_STATUS.get(status); |
| | | throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), m); |
| | | } |
| | | return true; |
| | | } |
| | | catch (final LockConflictException ce) |
| | | { |
| | | continue; |
| | | } |
| | | catch (final StorageRuntimeException de) |
| | | { |
| | | final LocalizableMessage m = ERR_JEB_COMPSCHEMA_CANNOT_STORE_EX.get(de.getMessage()); |
| | | throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), m, de); |
| | | } |
| | | } |
| | | return false; |
| | | } |
| | | |
| | | } |
| New file |
| | |
| | | /* |
| | | * CDDL HEADER START |
| | | * |
| | | * The contents of this file are subject to the terms of the |
| | | * Common Development and Distribution License, Version 1.0 only |
| | | * (the "License"). You may not use this file except in compliance |
| | | * with the License. |
| | | * |
| | | * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt |
| | | * or http://forgerock.org/license/CDDLv1.0.html. |
| | | * See the License for the specific language governing permissions |
| | | * and limitations under the License. |
| | | * |
| | | * When distributing Covered Code, include this CDDL HEADER in each |
| | | * file and include the License file at legal-notices/CDDLv1_0.txt. |
| | | * If applicable, add the following below this CDDL HEADER, with the |
| | | * fields enclosed by brackets "[]" replaced with your own identifying |
| | | * information: |
| | | * Portions Copyright [yyyy] [name of copyright owner] |
| | | * |
| | | * CDDL HEADER END |
| | | * |
| | | * |
| | | * Copyright 2006-2009 Sun Microsystems, Inc. |
| | | * Portions Copyright 2014 ForgeRock AS |
| | | */ |
| | | package org.opends.server.backends.pluggable; |
| | | |
| | | |
| | | |
| | | import org.opends.server.types.IdentifiedException; |
| | | import org.forgerock.i18n.LocalizableMessage; |
| | | |
| | | |
| | | /** |
| | | * This class defines an exception that may be thrown if a problem occurs in the |
| | | * JE backend database. |
| | | */ |
| | | public class JebException |
| | | extends IdentifiedException |
| | | { |
| | | /** |
| | | * The serial version identifier required to satisfy the compiler because this |
| | | * class extends <CODE>java.lang.Exception</CODE>, which implements the |
| | | * <CODE>java.io.Serializable</CODE> interface. This value was generated |
| | | * using the <CODE>serialver</CODE> command-line utility included with the |
| | | * Java SDK. |
| | | */ |
| | | static final long serialVersionUID = 3110979454298870834L; |
| | | |
| | | |
| | | |
| | | /** |
| | | * Creates a new JE backend exception. |
| | | */ |
| | | public JebException() |
| | | { |
| | | super(); |
| | | } |
| | | |
| | | |
| | | |
| | | /** |
| | | * Creates a new JE backend exception with the provided message. |
| | | * |
| | | * @param message The message that explains the problem that occurred. |
| | | */ |
| | | public JebException(LocalizableMessage message) |
| | | { |
| | | super(message); |
| | | } |
| | | |
| | | |
| | | |
| | | /** |
| | | * Creates a new JE backend exception with the provided message and root |
| | | * cause. |
| | | * |
| | | * @param message The message that explains the problem that occurred. |
| | | * @param cause The exception that was caught to trigger this exception. |
| | | */ |
| | | public JebException(LocalizableMessage message, Throwable cause) |
| | | { |
| | | super(message, cause); |
| | | } |
| | | |
| | | |
| | | |
| | | } |
| | | |
| New file |
| | |
| | | /* |
| | | * CDDL HEADER START |
| | | * |
| | | * The contents of this file are subject to the terms of the |
| | | * Common Development and Distribution License, Version 1.0 only |
| | | * (the "License"). You may not use this file except in compliance |
| | | * with the License. |
| | | * |
| | | * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt |
| | | * or http://forgerock.org/license/CDDLv1.0.html. |
| | | * See the License for the specific language governing permissions |
| | | * and limitations under the License. |
| | | * |
| | | * When distributing Covered Code, include this CDDL HEADER in each |
| | | * file and include the License file at legal-notices/CDDLv1_0.txt. |
| | | * If applicable, add the following below this CDDL HEADER, with the |
| | | * fields enclosed by brackets "[]" replaced with your own identifying |
| | | * information: |
| | | * Portions Copyright [yyyy] [name of copyright owner] |
| | | * |
| | | * CDDL HEADER END |
| | | * |
| | | * |
| | | * Copyright 2006-2010 Sun Microsystems, Inc. |
| | | * Portions Copyright 2014 ForgeRock AS |
| | | */ |
| | | package org.opends.server.backends.pluggable; |
| | | |
| | | import java.util.Iterator; |
| | | import java.util.TreeSet; |
| | | |
| | | import org.forgerock.opendj.ldap.ByteSequence; |
| | | import org.forgerock.opendj.ldap.ByteString; |
| | | import org.forgerock.opendj.ldap.ByteStringBuilder; |
| | | import org.opends.server.types.DN; |
| | | import org.opends.server.types.DirectoryException; |
| | | import org.opends.server.types.RDN; |
| | | import org.opends.server.util.StaticUtils; |
| | | |
| | | /** |
| | | * Handles the disk representation of LDAP data. |
| | | */ |
| | | public class JebFormat |
| | | { |
| | | |
| | | /** |
| | | * The format version used by this class to encode and decode a ByteString. |
| | | */ |
| | | public static final byte FORMAT_VERSION = 0x01; |
| | | |
| | | /** |
| | | * The ASN1 tag for the ByteString type. |
| | | */ |
| | | public static final byte TAG_DATABASE_ENTRY = 0x60; |
| | | |
| | | /** |
| | | * The ASN1 tag for the DirectoryServerEntry type. |
| | | */ |
| | | public static final byte TAG_DIRECTORY_SERVER_ENTRY = 0x61; |
| | | |
| | | /** |
| | | * Decode a long from a byte array, starting at start index and ending at end |
| | | * index. |
| | | * |
| | | * @param bytes |
| | | * The bytes value of the long. |
| | | * @param start |
| | | * the array index where to start computing the long |
| | | * @param end |
| | | * the array index exclusive where to end computing the long |
| | | * @return the long representation of the read bytes. |
| | | * @throws ArrayIndexOutOfBoundsException |
| | | * if the bytes array length is less than end. |
| | | */ |
| | | public static long toLong(byte[] bytes, int start, int end) |
| | | throws ArrayIndexOutOfBoundsException |
| | | { |
| | | long v = 0; |
| | | for (int i = start; i < end; i++) |
| | | { |
| | | v <<= 8; |
| | | v |= (bytes[i] & 0xFF); |
| | | } |
| | | return v; |
| | | } |
| | | |
| | | /** |
| | | * Decode an entry ID count from its database representation. |
| | | * |
| | | * @param bytes The database value of the entry ID count. |
| | | * @return The entry ID count. |
| | | * Cannot be negative if encoded with #entryIDUndefinedSizeToDatabase(long) |
| | | * @see #entryIDUndefinedSizeToDatabase(long) |
| | | */ |
| | | public static long entryIDUndefinedSizeFromDatabase(byte[] bytes) |
| | | { |
| | | if(bytes == null) |
| | | { |
| | | return 0; |
| | | } |
| | | |
| | | if(bytes.length == 8) |
| | | { |
| | | long v = 0; |
| | | v |= (bytes[0] & 0x7F); |
| | | for (int i = 1; i < 8; i++) |
| | | { |
| | | v <<= 8; |
| | | v |= (bytes[i] & 0xFF); |
| | | } |
| | | return v; |
| | | } |
| | | return Long.MAX_VALUE; |
| | | } |
| | | |
| | | /** |
| | | * Decode an array of entry ID values from its database representation. |
| | | * |
| | | * @param bytes The raw database value, null if there is no value and |
| | | * hence no entry IDs. Note that this method will throw an |
| | | * ArrayIndexOutOfBoundsException if the bytes array length is |
| | | * not a multiple of 8. |
| | | * @return An array of entry ID values. |
| | | * @see #entryIDListToDatabase(long[]) |
| | | */ |
| | | public static long[] entryIDListFromDatabase(ByteSequence bytes) |
| | | { |
| | | int count = bytes.length() / 8; |
| | | long[] entryIDList = new long[count]; |
| | | for (int pos = 0, i = 0; i < count; i++) |
| | | { |
| | | long v = 0; |
| | | v |= (bytes.byteAt(pos++) & 0xFFL) << 56; |
| | | v |= (bytes.byteAt(pos++) & 0xFFL) << 48; |
| | | v |= (bytes.byteAt(pos++) & 0xFFL) << 40; |
| | | v |= (bytes.byteAt(pos++) & 0xFFL) << 32; |
| | | v |= (bytes.byteAt(pos++) & 0xFFL) << 24; |
| | | v |= (bytes.byteAt(pos++) & 0xFFL) << 16; |
| | | v |= (bytes.byteAt(pos++) & 0xFFL) << 8; |
| | | v |= (bytes.byteAt(pos++) & 0xFFL); |
| | | entryIDList[i] = v; |
| | | } |
| | | |
| | | return entryIDList; |
| | | } |
| | | |
| | | /** |
| | | * Decode a integer array using the specified byte array read from DB. |
| | | * |
| | | * @param bytes The byte array. |
| | | * @return An integer array. |
| | | */ |
| | | public static int[] intArrayFromDatabaseBytes(byte[] bytes) { |
| | | byte[] decodedBytes = bytes; |
| | | |
| | | int count = decodedBytes.length / 8; |
| | | int[] entryIDList = new int[count]; |
| | | for (int pos = 0, i = 0; i < count; i++) { |
| | | int v = 0; |
| | | pos +=4; |
| | | v |= (decodedBytes[pos++] & 0xFFL) << 24; |
| | | v |= (decodedBytes[pos++] & 0xFFL) << 16; |
| | | v |= (decodedBytes[pos++] & 0xFFL) << 8; |
| | | v |= (decodedBytes[pos++] & 0xFFL); |
| | | entryIDList[i] = v; |
| | | } |
| | | |
| | | return entryIDList; |
| | | } |
| | | |
| | | /** |
| | | * Encode an entry ID value to its database representation. |
| | | * |
| | | * @param id The entry ID value to be encoded. |
| | | * @return The encoded database value of the entry ID. |
| | | * @see #entryIDFromDatabase(byte[]) |
| | | */ |
| | | public static ByteString entryIDToDatabase(long id) |
| | | { |
| | | return ByteString.valueOf(id); |
| | | } |
| | | |
| | | /** |
| | | * Encode an entry ID set count to its database representation. |
| | | * |
| | | * @param count The entry ID set count to be encoded. |
| | | * @return The encoded database value of the entry ID set count. |
| | | * @see #entryIDUndefinedSizeFromDatabase(byte[]) |
| | | */ |
| | | public static byte[] entryIDUndefinedSizeToDatabase(long count) |
| | | { |
| | | byte[] bytes = new byte[8]; |
| | | long v = count; |
| | | for (int i = 7; i >= 1; i--) |
| | | { |
| | | bytes[i] = (byte) (v & 0xFF); |
| | | v >>>= 8; |
| | | } |
| | | bytes[0] = (byte) ((v | 0x80) & 0xFF); |
| | | return bytes; |
| | | } |
| | | |
| | | /** |
| | | * Encode an array of entry ID values to its database representation. |
| | | * |
| | | * @param entryIDArray An array of entry ID values. |
| | | * @return The encoded database value. |
| | | * @see #entryIDListFromDatabase(byte[]) |
| | | */ |
| | | public static byte[] entryIDListToDatabase(long[] entryIDArray) |
| | | { |
| | | if (entryIDArray.length == 0) |
| | | { |
| | | // Zero values |
| | | return null; |
| | | } |
| | | |
| | | byte[] bytes = new byte[8*entryIDArray.length]; |
| | | for (int pos = 0, i = 0; i < entryIDArray.length; i++) |
| | | { |
| | | long v = entryIDArray[i]; |
| | | bytes[pos++] = (byte) ((v >>> 56) & 0xFF); |
| | | bytes[pos++] = (byte) ((v >>> 48) & 0xFF); |
| | | bytes[pos++] = (byte) ((v >>> 40) & 0xFF); |
| | | bytes[pos++] = (byte) ((v >>> 32) & 0xFF); |
| | | bytes[pos++] = (byte) ((v >>> 24) & 0xFF); |
| | | bytes[pos++] = (byte) ((v >>> 16) & 0xFF); |
| | | bytes[pos++] = (byte) ((v >>> 8) & 0xFF); |
| | | bytes[pos++] = (byte) (v & 0xFF); |
| | | } |
| | | |
| | | return bytes; |
| | | } |
| | | |
| | | /** |
| | | * Decode a DN value from its database key representation. |
| | | * |
| | | * @param dnKey The database key value of the DN. |
| | | * @param prefix The DN to prefix the decoded DN value. |
| | | * @return The decoded DN value. |
| | | * @throws DirectoryException if an error occurs while decoding the DN value. |
| | | * @see #dnToDNKey(DN, int) |
| | | */ |
| | | public static DN dnFromDNKey(ByteSequence dnKey, DN prefix) throws DirectoryException |
| | | { |
| | | DN dn = prefix; |
| | | boolean escaped = false; |
| | | ByteStringBuilder buffer = new ByteStringBuilder(); |
| | | for(int i = 0; i < dnKey.length(); i++) |
| | | { |
| | | if(dnKey.byteAt(i) == 0x5C) |
| | | { |
| | | escaped = true; |
| | | continue; |
| | | } |
| | | else if(!escaped && dnKey.byteAt(i) == 0x01) |
| | | { |
| | | buffer.append(0x01); |
| | | escaped = false; |
| | | continue; |
| | | } |
| | | else if(!escaped && dnKey.byteAt(i) == 0x00) |
| | | { |
| | | if(buffer.length() > 0) |
| | | { |
| | | dn = dn.child(RDN.decode(buffer.toString())); |
| | | buffer.clear(); |
| | | } |
| | | } |
| | | else |
| | | { |
| | | if(escaped) |
| | | { |
| | | buffer.append(0x5C); |
| | | escaped = false; |
| | | } |
| | | buffer.append(dnKey.byteAt(i)); |
| | | } |
| | | } |
| | | |
| | | if(buffer.length() > 0) |
| | | { |
| | | dn = dn.child(RDN.decode(buffer.toString())); |
| | | } |
| | | |
| | | return dn; |
| | | } |
| | | |
| | | /** |
| | | * Find the length of bytes that represents the superior DN of the given |
| | | * DN key. The superior DN is represented by the initial bytes of the DN key. |
| | | * |
| | | * @param dnKey The database key value of the DN. |
| | | * @param offset Starting position in the database key data. |
| | | * @param length The length of the database key data. |
| | | * @return The length of the superior DN or -1 if the given dn is the |
| | | * root DN or 0 if the superior DN is removed. |
| | | */ |
| | | public static int findDNKeyParent(byte[] dnKey, int offset, int length) |
| | | { |
| | | if(length == 0) |
| | | { |
| | | // This is the root or base DN |
| | | return -1; |
| | | } |
| | | |
| | | // We will walk backwords through the buffer and find the first |
| | | // unescaped comma |
| | | for(int i = offset+length - 1; i >= offset; i--) |
| | | { |
| | | if(dnKey[i] == 0x00 && i-1 >= offset && dnKey[i-1] != 0x5C) |
| | | { |
| | | return i; |
| | | } |
| | | } |
| | | return offset; |
| | | } |
| | | |
| | | public static int findDNKeyParent(ByteSequence dnKey) |
| | | { |
| | | if (dnKey.length() == 0) |
| | | { |
| | | // This is the root or base DN |
| | | return -1; |
| | | } |
| | | |
| | | // We will walk backwords through the buffer and find the first |
| | | // unescaped comma |
| | | for (int i = dnKey.length() - 1; i >= 0; i--) |
| | | { |
| | | if (dnKey.byteAt(i) == 0x00 && i - 1 >= 0 && dnKey.byteAt(i - 1) != 0x5C) |
| | | { |
| | | return i; |
| | | } |
| | | } |
| | | return 0; |
| | | } |
| | | |
| | | /** |
| | | * Create a DN database key from an entry DN. |
| | | * @param dn The entry DN. |
| | | * @param prefixRDNs The number of prefix RDNs to remove from the encoded |
| | | * representation. |
| | | * @return A ByteString containing the key. |
| | | * @see #dnFromDNKey(byte[], int, int, DN) |
| | | */ |
| | | public static ByteString dnToDNKey(DN dn, int prefixRDNs) |
| | | { |
| | | StringBuilder buffer = new StringBuilder(); |
| | | for (int i = dn.size() - prefixRDNs - 1; i >= 0; i--) |
| | | { |
| | | buffer.append('\u0000'); |
| | | formatRDNKey(dn.getRDN(i), buffer); |
| | | } |
| | | |
| | | return ByteString.wrap(StaticUtils.getBytes(buffer.toString())); |
| | | } |
| | | |
| | | private static void formatRDNKey(RDN rdn, StringBuilder buffer) |
| | | { |
| | | if (!rdn.isMultiValued()) |
| | | { |
| | | rdn.toNormalizedString(buffer); |
| | | } |
| | | else |
| | | { |
| | | TreeSet<String> rdnElementStrings = new TreeSet<String>(); |
| | | |
| | | for (int i=0; i < rdn.getNumValues(); i++) |
| | | { |
| | | StringBuilder b2 = new StringBuilder(); |
| | | rdn.getNormalizedAVAString(i, b2); |
| | | rdnElementStrings.add(b2.toString()); |
| | | } |
| | | |
| | | Iterator<String> iterator = rdnElementStrings.iterator(); |
| | | buffer.append(iterator.next().replace("\u0001", "\\\u0001")); |
| | | while (iterator.hasNext()) |
| | | { |
| | | buffer.append('\u0001'); |
| | | buffer.append(iterator.next().replace("\u0001", "\\\u0001")); |
| | | } |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | /* |
| | | * CDDL HEADER START |
| | | * |
| | | * The contents of this file are subject to the terms of the |
| | | * Common Development and Distribution License, Version 1.0 only |
| | | * (the "License"). You may not use this file except in compliance |
| | | * with the License. |
| | | * |
| | | * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt |
| | | * or http://forgerock.org/license/CDDLv1.0.html. |
| | | * See the License for the specific language governing permissions |
| | | * and limitations under the License. |
| | | * |
| | | * When distributing Covered Code, include this CDDL HEADER in each |
| | | * file and include the License file at legal-notices/CDDLv1_0.txt. |
| | | * If applicable, add the following below this CDDL HEADER, with the |
| | | * fields enclosed by brackets "[]" replaced with your own identifying |
| | | * information: |
| | | * Portions Copyright [yyyy] [name of copyright owner] |
| | | * |
| | | * CDDL HEADER END |
| | | * |
| | | * Copyright 2011-2014 ForgeRock AS |
| | | */ |
| | | package org.opends.server.backends.pluggable; |
| | | |
| | | import java.util.List; |
| | | |
| | | import org.forgerock.opendj.ldap.ByteSequence; |
| | | import org.forgerock.opendj.ldap.ByteString; |
| | | import org.forgerock.opendj.ldap.ConditionResult; |
| | | import org.forgerock.opendj.ldap.spi.IndexingOptions; |
| | | import org.opends.server.backends.pluggable.BackendImpl.ReadableStorage; |
| | | import org.opends.server.backends.pluggable.BackendImpl.Storage; |
| | | import org.opends.server.backends.pluggable.BackendImpl.StorageRuntimeException; |
| | | import org.opends.server.backends.pluggable.BackendImpl.TreeName; |
| | | import org.opends.server.backends.pluggable.BackendImpl.WriteableStorage; |
| | | import org.opends.server.types.DirectoryException; |
| | | import org.opends.server.types.Entry; |
| | | import org.opends.server.types.Modification; |
| | | |
| | | import com.sleepycat.je.PreloadConfig; |
| | | import com.sleepycat.je.PreloadStats; |
| | | |
| | | /** |
| | | * A null index which replaces id2children and id2subtree when they have been |
| | | * disabled. |
| | | */ |
| | | final class NullIndex extends Index |
| | | { |
| | | |
| | | /** |
| | | * Create a new null index object. |
| | | * |
| | | * @param name |
| | | * The name of the index database within the entryContainer. |
| | | * @param indexer |
| | | * The indexer object to construct index keys from LDAP attribute |
| | | * values. |
| | | * @param state |
| | | * The state database to persist index state info. |
| | | * @param storage |
| | | * The JE Storage |
| | | * @param entryContainer |
| | | * The database entryContainer holding this index. |
| | | * @throws StorageRuntimeException |
| | | * If an error occurs in the JE database. |
| | | */ |
| | | public NullIndex(TreeName name, Indexer indexer, State state, Storage storage, |
| | | EntryContainer entryContainer) throws StorageRuntimeException |
| | | { |
| | | super(name, indexer, state, 0, 0, false, storage, entryContainer); |
| | | } |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | void updateKey(WriteableStorage txn, ByteString key, EntryIDSet deletedIDs, |
| | | EntryIDSet addedIDs) throws StorageRuntimeException |
| | | { |
| | | // Do nothing. |
| | | } |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public void delete(IndexBuffer buffer, ByteString keyBytes) |
| | | { |
| | | // Do nothing. |
| | | } |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public ConditionResult containsID(ReadableStorage txn, ByteString key, |
| | | EntryID entryID) throws StorageRuntimeException |
| | | { |
| | | return ConditionResult.UNDEFINED; |
| | | } |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public EntryIDSet readKey(ByteSequence key, ReadableStorage txn) |
| | | { |
| | | return new EntryIDSet(); |
| | | } |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public void writeKey(WriteableStorage txn, ByteString key, |
| | | EntryIDSet entryIDList) throws StorageRuntimeException |
| | | { |
| | | // Do nothing. |
| | | } |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public EntryIDSet readRange(ByteSequence lower, ByteSequence upper, |
| | | boolean lowerIncluded, boolean upperIncluded) |
| | | { |
| | | return new EntryIDSet(); |
| | | } |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public int getEntryLimitExceededCount() |
| | | { |
| | | return 0; |
| | | } |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public void closeCursor() throws StorageRuntimeException |
| | | { |
| | | // Do nothing. |
| | | } |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public void addEntry(IndexBuffer buffer, EntryID entryID, Entry entry, IndexingOptions options) |
| | | throws StorageRuntimeException, DirectoryException |
| | | { |
| | | // Do nothing. |
| | | } |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public void removeEntry(IndexBuffer buffer, EntryID entryID, Entry entry, IndexingOptions options) |
| | | throws StorageRuntimeException, DirectoryException |
| | | { |
| | | // Do nothing. |
| | | } |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public void modifyEntry(IndexBuffer buffer, EntryID entryID, Entry oldEntry, |
| | | Entry newEntry, List<Modification> mods, IndexingOptions options) throws StorageRuntimeException |
| | | { |
| | | // Do nothing. |
| | | } |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public boolean setIndexEntryLimit(int indexEntryLimit) |
| | | { |
| | | return false; |
| | | } |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public int getIndexEntryLimit() |
| | | { |
| | | return 0; |
| | | } |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public void setTrusted(WriteableStorage txn, boolean trusted) |
| | | throws StorageRuntimeException |
| | | { |
| | | // Do nothing. |
| | | } |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public boolean isTrusted() |
| | | { |
| | | return true; |
| | | } |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public boolean isRebuildRunning() |
| | | { |
| | | return false; |
| | | } |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public void setRebuildStatus(boolean rebuildRunning) |
| | | { |
| | | // Do nothing. |
| | | } |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public boolean getMaintainCount() |
| | | { |
| | | return false; |
| | | } |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public void open() throws StorageRuntimeException |
| | | { |
| | | // Do nothing. |
| | | } |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public void close() throws StorageRuntimeException |
| | | { |
| | | // Do nothing. |
| | | } |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | protected void put(WriteableStorage txn, ByteSequence key, ByteSequence value) throws StorageRuntimeException |
| | | { |
| | | } |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | protected ByteString read(ReadableStorage txn, ByteSequence key, boolean isRMW) throws StorageRuntimeException |
| | | { |
| | | return null; |
| | | } |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | protected boolean insert(WriteableStorage txn, ByteString key, |
| | | ByteString value) throws StorageRuntimeException |
| | | { |
| | | return true; |
| | | } |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | protected boolean delete(WriteableStorage txn, ByteSequence key) |
| | | throws StorageRuntimeException |
| | | { |
| | | return true; |
| | | } |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public long getRecordCount() throws StorageRuntimeException |
| | | { |
| | | return 0; |
| | | } |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public PreloadStats preload(PreloadConfig config) throws StorageRuntimeException |
| | | { |
| | | return new PreloadStats(); |
| | | } |
| | | |
| | | } |
| New file |
| | |
| | | /* |
| | | * CDDL HEADER START |
| | | * |
| | | * The contents of this file are subject to the terms of the |
| | | * Common Development and Distribution License, Version 1.0 only |
| | | * (the "License"). You may not use this file except in compliance |
| | | * with the License. |
| | | * |
| | | * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt |
| | | * or http://forgerock.org/license/CDDLv1.0.html. |
| | | * See the License for the specific language governing permissions |
| | | * and limitations under the License. |
| | | * |
| | | * When distributing Covered Code, include this CDDL HEADER in each |
| | | * file and include the License file at legal-notices/CDDLv1_0.txt. |
| | | * If applicable, add the following below this CDDL HEADER, with the |
| | | * fields enclosed by brackets "[]" replaced with your own identifying |
| | | * information: |
| | | * Portions Copyright [yyyy] [name of copyright owner] |
| | | * |
| | | * CDDL HEADER END |
| | | * |
| | | * |
| | | * Copyright 2006-2008 Sun Microsystems, Inc. |
| | | * Portions Copyright 2014 ForgeRock AS |
| | | */ |
| | | package org.opends.server.backends.pluggable; |
| | | |
| | | import java.util.Collections; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import java.util.Set; |
| | | |
| | | import org.forgerock.opendj.ldap.ByteString; |
| | | import org.forgerock.opendj.ldap.spi.IndexingOptions; |
| | | import org.opends.server.types.Attribute; |
| | | import org.opends.server.types.AttributeType; |
| | | import org.opends.server.types.Entry; |
| | | import org.opends.server.types.Modification; |
| | | |
| | | /** |
| | | * An implementation of an Indexer for attribute presence. |
| | | */ |
| | | public class PresenceIndexer extends Indexer |
| | | { |
| | | /** The key bytes used for the presence index. */ |
| | | static final byte[] presenceKeyBytes = "+".getBytes(); |
| | | |
| | | /** The key bytes used for the presence index as a {@link ByteString}. */ |
| | | static final ByteString presenceKey = ByteString.wrap(presenceKeyBytes); |
| | | |
| | | /** The attribute type for which this instance will generate index keys. */ |
| | | private AttributeType attributeType; |
| | | |
| | | /** |
| | | * Create a new attribute presence indexer. |
| | | * @param attributeType The attribute type for which the indexer |
| | | * is required. |
| | | */ |
| | | public PresenceIndexer(AttributeType attributeType) |
| | | { |
| | | this.attributeType = attributeType; |
| | | } |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public String toString() |
| | | { |
| | | return attributeType.getNameOrOID() + ".presence"; |
| | | } |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public void indexEntry(Entry entry, Set<ByteString> keys, IndexingOptions options) |
| | | { |
| | | List<Attribute> attrList = entry.getAttribute(attributeType); |
| | | if (attrList != null) |
| | | { |
| | | if (!attrList.isEmpty()) |
| | | { |
| | | keys.add(presenceKey); |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public void replaceEntry(Entry oldEntry, Entry newEntry, |
| | | Map<ByteString, Boolean> modifiedKeys, IndexingOptions options) |
| | | { |
| | | modifyEntry(oldEntry, newEntry, Collections.<Modification>emptyList(), modifiedKeys, options); |
| | | } |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public void modifyEntry(Entry oldEntry, Entry newEntry, |
| | | List<Modification> mods, |
| | | Map<ByteString, Boolean> modifiedKeys, IndexingOptions options) |
| | | { |
| | | List<Attribute> newAttributes = newEntry.getAttribute(attributeType, true); |
| | | List<Attribute> oldAttributes = oldEntry.getAttribute(attributeType, true); |
| | | if(oldAttributes == null) |
| | | { |
| | | if(newAttributes != null) |
| | | { |
| | | modifiedKeys.put(presenceKey, true); |
| | | } |
| | | } |
| | | else |
| | | { |
| | | if(newAttributes == null) |
| | | { |
| | | modifiedKeys.put(presenceKey, false); |
| | | } |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | /* |
| | | * CDDL HEADER START |
| | | * |
| | | * The contents of this file are subject to the terms of the |
| | | * Common Development and Distribution License, Version 1.0 only |
| | | * (the "License"). You may not use this file except in compliance |
| | | * with the License. |
| | | * |
| | | * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt |
| | | * or http://forgerock.org/license/CDDLv1.0.html. |
| | | * See the License for the specific language governing permissions |
| | | * and limitations under the License. |
| | | * |
| | | * When distributing Covered Code, include this CDDL HEADER in each |
| | | * file and include the License file at legal-notices/CDDLv1_0.txt. |
| | | * If applicable, add the following below this CDDL HEADER, with the |
| | | * fields enclosed by brackets "[]" replaced with your own identifying |
| | | * information: |
| | | * Portions Copyright [yyyy] [name of copyright owner] |
| | | * |
| | | * CDDL HEADER END |
| | | * |
| | | * |
| | | * Copyright 2006-2009 Sun Microsystems, Inc. |
| | | * Portions Copyright 2011-2014 ForgeRock AS |
| | | */ |
| | | package org.opends.server.backends.pluggable; |
| | | |
| | | import org.opends.server.types.DN; |
| | | |
| | | import java.util.ArrayList; |
| | | |
| | | /** |
| | | * Configuration for the indexType rebuild process. |
| | | */ |
| | | public class RebuildConfig |
| | | { |
| | | /** |
| | | * Identifies how indexes will be selected for rebuild. |
| | | */ |
| | | public static enum RebuildMode |
| | | { |
| | | /** |
| | | * Rebuild all indexes, including system indexes. |
| | | */ |
| | | ALL, |
| | | |
| | | /** |
| | | * Rebuild all degraded indexes, including system indexes. |
| | | */ |
| | | DEGRADED, |
| | | |
| | | /** |
| | | * Rebuild used defined list of indexes. |
| | | */ |
| | | USER_DEFINED; |
| | | } |
| | | |
| | | /** |
| | | * The base DN to rebuild. |
| | | */ |
| | | private DN baseDN; |
| | | |
| | | /** |
| | | * The names of indexes to rebuild. |
| | | */ |
| | | private ArrayList<String> rebuildList; |
| | | |
| | | private RebuildMode rebuildMode = RebuildMode.USER_DEFINED; |
| | | |
| | | private String tmpDirectory; |
| | | |
| | | private boolean isClearDegradedState; |
| | | |
| | | /** |
| | | * Create a new rebuild configuration. |
| | | */ |
| | | public RebuildConfig() |
| | | { |
| | | rebuildList = new ArrayList<String>(); |
| | | } |
| | | |
| | | /** |
| | | * Get the base DN to rebuild. |
| | | * |
| | | * @return The base DN to rebuild. |
| | | */ |
| | | public DN getBaseDN() |
| | | { |
| | | return baseDN; |
| | | } |
| | | |
| | | /** |
| | | * Set the base DN to rebuild. |
| | | * |
| | | * @param baseDN |
| | | * The base DN to rebuild. |
| | | */ |
| | | public void setBaseDN(DN baseDN) |
| | | { |
| | | this.baseDN = baseDN; |
| | | } |
| | | |
| | | /** |
| | | * Get the list of indexes to rebuild in this configuration. |
| | | * |
| | | * @return The list of indexes to rebuild. |
| | | */ |
| | | public ArrayList<String> getRebuildList() |
| | | { |
| | | return rebuildList; |
| | | } |
| | | |
| | | /** |
| | | * Add an index to be rebuilt into the configuration. Duplicate index names |
| | | * will be ignored. Adding an index that causes a mix of complete and partial |
| | | * rebuild for the same attribute index in the configuration will remove the |
| | | * partial and just keep the complete attribute index name. (ie. uid and |
| | | * uid.presence). |
| | | * |
| | | * @param index |
| | | * The index to add. |
| | | */ |
| | | public void addRebuildIndex(String index) |
| | | { |
| | | String[] newIndexParts = index.split("\\."); |
| | | |
| | | for (String s : new ArrayList<String>(rebuildList)) |
| | | { |
| | | String[] existingIndexParts = s.split("\\."); |
| | | if (existingIndexParts[0].equalsIgnoreCase(newIndexParts[0])) |
| | | { |
| | | if (newIndexParts.length == 1 && existingIndexParts.length == 1) |
| | | { |
| | | return; |
| | | } |
| | | else if (newIndexParts.length > 1 && existingIndexParts.length == 1) |
| | | { |
| | | return; |
| | | } |
| | | else if (newIndexParts.length == 1 && existingIndexParts.length > 1) |
| | | { |
| | | rebuildList.remove(s); |
| | | } |
| | | else if (newIndexParts[1].equalsIgnoreCase(existingIndexParts[1])) |
| | | { |
| | | return; |
| | | } |
| | | } |
| | | } |
| | | |
| | | this.rebuildList.add(index); |
| | | } |
| | | |
| | | /** |
| | | * Check the given config for conflicts with this config. A conflict is |
| | | * detected if both configs specify the same indexType/database to be rebuilt. |
| | | * |
| | | * @param config |
| | | * The rebuild config to check against. |
| | | * @return the name of the indexType causing the conflict or null if no |
| | | * conflict is detected. |
| | | */ |
| | | public String checkConflicts(RebuildConfig config) |
| | | { |
| | | //If they specify different base DNs, no conflicts can occur. |
| | | if (this.baseDN.equals(config.baseDN)) |
| | | { |
| | | for (String thisIndex : this.rebuildList) |
| | | { |
| | | for (String thatIndex : config.rebuildList) |
| | | { |
| | | String[] existingIndexParts = thisIndex.split("\\."); |
| | | String[] newIndexParts = thatIndex.split("\\."); |
| | | if (existingIndexParts[0].equalsIgnoreCase(newIndexParts[0])) |
| | | { |
| | | if (newIndexParts.length == 1 && existingIndexParts.length == 1) |
| | | { |
| | | return thatIndex; |
| | | } |
| | | else if (newIndexParts.length > 1 && existingIndexParts.length == 1) |
| | | { |
| | | return thatIndex; |
| | | } |
| | | else if (newIndexParts.length == 1 && existingIndexParts.length > 1) |
| | | { |
| | | return thatIndex; |
| | | } |
| | | else if (newIndexParts[1].equalsIgnoreCase(existingIndexParts[1])) |
| | | { |
| | | return thatIndex; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | return null; |
| | | } |
| | | |
| | | /** |
| | | * Test if this rebuild config includes any system indexes to rebuild. |
| | | * |
| | | * @return True if rebuilding of system indexes are included. False otherwise. |
| | | * @throws InitializationException |
| | | */ |
| | | public boolean includesSystemIndex() |
| | | { |
| | | for (String index : rebuildList) |
| | | { |
| | | // Removed because the id2entry is not A system indexes is THE |
| | | // primary system index. It cannot be rebuilt. |
| | | /*if (index.equalsIgnoreCase("id2entry")) |
| | | { |
| | | return true; |
| | | }*/ |
| | | if (index.equalsIgnoreCase("dn2id")) |
| | | { |
| | | return true; |
| | | } |
| | | if (index.equalsIgnoreCase("dn2uri")) |
| | | { |
| | | return true; |
| | | } |
| | | } |
| | | |
| | | return false; |
| | | } |
| | | |
| | | /** |
| | | * Set the temporary directory to the specified path. |
| | | * |
| | | * @param path |
| | | * The path to set the temporary directory to. |
| | | */ |
| | | public void setTmpDirectory(String path) |
| | | { |
| | | tmpDirectory = path; |
| | | } |
| | | |
| | | /** |
| | | * Return the temporary directory path. |
| | | * |
| | | * @return The temporary directory string. |
| | | */ |
| | | public String getTmpDirectory() |
| | | { |
| | | return tmpDirectory; |
| | | } |
| | | |
| | | /** |
| | | * Sets the rebuild mode. |
| | | * |
| | | * @param mode |
| | | * The new rebuild mode. |
| | | */ |
| | | public void setRebuildMode(RebuildMode mode) |
| | | { |
| | | rebuildMode = mode; |
| | | } |
| | | |
| | | /** |
| | | * Returns the rebuild mode. |
| | | * |
| | | * @return The rebuild mode. |
| | | */ |
| | | public RebuildMode getRebuildMode() |
| | | { |
| | | return rebuildMode; |
| | | } |
| | | |
| | | /** |
| | | * Returns {@code true} if indexes should be forcefully marked as valid even |
| | | * if they are currently degraded. |
| | | * |
| | | * @return {@code true} if index should be forcefully marked as valid. |
| | | */ |
| | | public boolean isClearDegradedState() |
| | | { |
| | | return isClearDegradedState; |
| | | } |
| | | |
| | | /** |
| | | * Sets the 'clear degraded index' status. |
| | | * |
| | | * @param isClearDegradedState |
| | | * {@code true} if indexes should be forcefully marked as valid even |
| | | * if they are currently degraded. |
| | | */ |
| | | public void isClearDegradedState(boolean isClearDegradedState) |
| | | { |
| | | this.isClearDegradedState = isClearDegradedState; |
| | | } |
| | | |
| | | } |
| New file |
| | |
| | | /* |
| | | * CDDL HEADER START |
| | | * |
| | | * The contents of this file are subject to the terms of the |
| | | * Common Development and Distribution License, Version 1.0 only |
| | | * (the "License"). You may not use this file except in compliance |
| | | * with the License. |
| | | * |
| | | * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt |
| | | * or http://forgerock.org/license/CDDLv1.0.html. |
| | | * See the License for the specific language governing permissions |
| | | * and limitations under the License. |
| | | * |
| | | * When distributing Covered Code, include this CDDL HEADER in each |
| | | * file and include the License file at legal-notices/CDDLv1_0.txt. |
| | | * If applicable, add the following below this CDDL HEADER, with the |
| | | * fields enclosed by brackets "[]" replaced with your own identifying |
| | | * information: |
| | | * Portions Copyright [yyyy] [name of copyright owner] |
| | | * |
| | | * CDDL HEADER END |
| | | * |
| | | * |
| | | * Copyright 2006-2010 Sun Microsystems, Inc. |
| | | * Portions Copyright 2011-2014 ForgeRock AS |
| | | */ |
| | | package org.opends.server.backends.pluggable; |
| | | |
| | | import java.io.File; |
| | | import java.util.*; |
| | | import java.util.concurrent.ConcurrentHashMap; |
| | | import java.util.concurrent.atomic.AtomicLong; |
| | | |
| | | import org.forgerock.i18n.LocalizableMessage; |
| | | import org.forgerock.i18n.slf4j.LocalizedLogger; |
| | | import org.forgerock.opendj.config.server.ConfigException; |
| | | import org.forgerock.opendj.ldap.ResultCode; |
| | | import org.opends.server.admin.server.ConfigurationChangeListener; |
| | | import org.opends.server.admin.std.server.LocalDBBackendCfg; |
| | | import org.opends.server.api.Backend; |
| | | import org.opends.server.backends.pluggable.BackendImpl.Storage; |
| | | import org.opends.server.backends.pluggable.BackendImpl.StorageRuntimeException; |
| | | import org.opends.server.core.DirectoryServer; |
| | | import org.opends.server.monitors.DatabaseEnvironmentMonitor; |
| | | import org.opends.server.types.ConfigChangeResult; |
| | | import org.opends.server.types.DN; |
| | | import org.opends.server.types.FilePermission; |
| | | import org.opends.server.types.InitializationException; |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | import static org.opends.messages.ConfigMessages.*; |
| | | import static org.opends.messages.JebMessages.*; |
| | | import static org.opends.server.util.StaticUtils.*; |
| | | |
| | | /** |
| | | * Wrapper class for the JE environment. Root container holds all the entry |
| | | * containers for each base DN. It also maintains all the openings and closings |
| | | * of the entry containers. |
| | | */ |
| | | public class RootContainer |
| | | implements ConfigurationChangeListener<LocalDBBackendCfg> |
| | | { |
| | | private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); |
| | | |
| | | /** The JE database environment. */ |
| | | private Storage storage; |
| | | |
| | | /** Used to force a checkpoint during import. */ |
| | | private final CheckpointConfig importForceCheckPoint = new CheckpointConfig(); |
| | | |
| | | /** The backend configuration. */ |
| | | private LocalDBBackendCfg config; |
| | | |
| | | /** The backend to which this entry root container belongs. */ |
| | | private final Backend<?> backend; |
| | | |
| | | /** The database environment monitor for this JE environment. */ |
| | | private DatabaseEnvironmentMonitor monitor; |
| | | |
| | | /** The base DNs contained in this root container. */ |
| | | private final ConcurrentHashMap<DN, EntryContainer> entryContainers = new ConcurrentHashMap<DN, EntryContainer>(); |
| | | |
| | | /** The cached value of the next entry identifier to be assigned. */ |
| | | private AtomicLong nextid = new AtomicLong(1); |
| | | |
| | | /** The compressed schema manager for this backend. */ |
| | | private JECompressedSchema compressedSchema; |
| | | |
| | | |
| | | |
| | | /** |
| | | * Creates a new RootContainer object. Each root container represents a JE |
| | | * environment. |
| | | * |
| | | * @param config The configuration of the JE backend. |
| | | * @param backend A reference to the JE back end that is creating this |
| | | * root container. |
| | | */ |
| | | public RootContainer(Backend<?> backend, LocalDBBackendCfg config) |
| | | { |
| | | this.backend = backend; |
| | | this.config = config; |
| | | |
| | | getMonitorProvider().enableFilterUseStats(config.isIndexFilterAnalyzerEnabled()); |
| | | getMonitorProvider().setMaxEntries(config.getIndexFilterAnalyzerMaxFilters()); |
| | | |
| | | config.addLocalDBChangeListener(this); |
| | | importForceCheckPoint.setForce(true); |
| | | } |
| | | |
| | | /** |
| | | * Opens the root container using the JE configuration object provided. |
| | | * |
| | | * @param envConfig The JE environment configuration. |
| | | * @throws StorageRuntimeException If a database error occurs when creating |
| | | * the environment. |
| | | * @throws InitializationException If an initialization error occurs while |
| | | * creating the environment. |
| | | * @throws ConfigException If an configuration error occurs while |
| | | * creating the environment. |
| | | */ |
| | | public void open(EnvironmentConfig envConfig) |
| | | throws StorageRuntimeException, InitializationException, ConfigException |
| | | { |
| | | // Determine the backend database directory. |
| | | File parentDirectory = getFileForPath(config.getDBDirectory()); |
| | | File backendDirectory = new File(parentDirectory, config.getBackendId()); |
| | | |
| | | // Create the directory if it doesn't exist. |
| | | if (!backendDirectory.exists()) |
| | | { |
| | | if(!backendDirectory.mkdirs()) |
| | | { |
| | | LocalizableMessage message = |
| | | ERR_JEB_CREATE_FAIL.get(backendDirectory.getPath()); |
| | | throw new ConfigException(message); |
| | | } |
| | | } |
| | | //Make sure the directory is valid. |
| | | else if (!backendDirectory.isDirectory()) |
| | | { |
| | | throw new ConfigException(ERR_JEB_DIRECTORY_INVALID.get(backendDirectory.getPath())); |
| | | } |
| | | |
| | | FilePermission backendPermission; |
| | | try |
| | | { |
| | | backendPermission = |
| | | FilePermission.decodeUNIXMode(config.getDBDirectoryPermissions()); |
| | | } |
| | | catch(Exception e) |
| | | { |
| | | throw new ConfigException(ERR_CONFIG_BACKEND_MODE_INVALID.get(config.dn())); |
| | | } |
| | | |
| | | //Make sure the mode will allow the server itself access to |
| | | //the database |
| | | if(!backendPermission.isOwnerWritable() || |
| | | !backendPermission.isOwnerReadable() || |
| | | !backendPermission.isOwnerExecutable()) |
| | | { |
| | | LocalizableMessage message = ERR_CONFIG_BACKEND_INSANE_MODE.get( |
| | | config.getDBDirectoryPermissions()); |
| | | throw new ConfigException(message); |
| | | } |
| | | |
| | | // Get the backend database backendDirectory permissions and apply |
| | | if(FilePermission.canSetPermissions()) |
| | | { |
| | | try |
| | | { |
| | | if(!FilePermission.setPermissions(backendDirectory, backendPermission)) |
| | | { |
| | | logger.warn(WARN_JEB_UNABLE_SET_PERMISSIONS, backendPermission, backendDirectory); |
| | | } |
| | | } |
| | | catch(Exception e) |
| | | { |
| | | // Log an warning that the permissions were not set. |
| | | logger.warn(WARN_JEB_SET_PERMISSIONS_FAILED, backendDirectory, e); |
| | | } |
| | | } |
| | | |
| | | // Open the database environment |
| | | storage = new Storage(backendDirectory, envConfig); |
| | | |
| | | if (logger.isTraceEnabled()) |
| | | { |
| | | logger.trace("JE (%s) environment opened with the following config: %n%s", |
| | | JEVersion.CURRENT_VERSION, storage.getConfig()); |
| | | |
| | | // Get current size of heap in bytes |
| | | long heapSize = Runtime.getRuntime().totalMemory(); |
| | | |
| | | // Get maximum size of heap in bytes. The heap cannot grow beyond this size. |
| | | // Any attempt will result in an OutOfMemoryException. |
| | | long heapMaxSize = Runtime.getRuntime().maxMemory(); |
| | | |
| | | // Get amount of free memory within the heap in bytes. This size will increase |
| | | // after garbage collection and decrease as new objects are created. |
| | | long heapFreeSize = Runtime.getRuntime().freeMemory(); |
| | | |
| | | logger.trace("Current size of heap: %d bytes", heapSize); |
| | | logger.trace("Max size of heap: %d bytes", heapMaxSize); |
| | | logger.trace("Free memory in heap: %d bytes", heapFreeSize); |
| | | } |
| | | |
| | | compressedSchema = new JECompressedSchema(storage); |
| | | openAndRegisterEntryContainers(config.getBaseDN()); |
| | | } |
| | | |
| | | /** |
| | | * Opens the entry container for a base DN. If the entry container does not |
| | | * exist for the base DN, it will be created. The entry container will be |
| | | * opened with the same mode as the root container. Any entry containers |
| | | * opened in a read only root container will also be read only. Any entry |
| | | * containers opened in a non transactional root container will also be non |
| | | * transactional. |
| | | * |
| | | * @param baseDN The base DN of the entry container to open. |
| | | * @param name The name of the entry container or <CODE>NULL</CODE> to open |
| | | * the default entry container for the given base DN. |
| | | * @return The opened entry container. |
| | | * @throws StorageRuntimeException If an error occurs while opening the entry |
| | | * container. |
| | | * @throws ConfigException If an configuration error occurs while opening |
| | | * the entry container. |
| | | */ |
| | | public EntryContainer openEntryContainer(DN baseDN, String name) |
| | | throws StorageRuntimeException, ConfigException |
| | | { |
| | | String databasePrefix; |
| | | if(name == null || name.equals("")) |
| | | { |
| | | databasePrefix = baseDN.toNormalizedString(); |
| | | } |
| | | else |
| | | { |
| | | databasePrefix = name; |
| | | } |
| | | |
| | | EntryContainer ec = new EntryContainer(baseDN, databasePrefix, |
| | | backend, config, storage, this); |
| | | ec.open(); |
| | | return ec; |
| | | } |
| | | |
| | | /** |
| | | * Registers the entry container for a base DN. |
| | | * |
| | | * @param baseDN The base DN of the entry container to close. |
| | | * @param entryContainer The entry container to register for the baseDN. |
| | | * @throws InitializationException If an error occurs while opening the |
| | | * entry container. |
| | | */ |
| | | public void registerEntryContainer(DN baseDN, EntryContainer entryContainer) |
| | | throws InitializationException |
| | | { |
| | | EntryContainer ec1 = this.entryContainers.get(baseDN); |
| | | |
| | | // If an entry container for this baseDN is already open we don't allow |
| | | // another to be opened. |
| | | if (ec1 != null) |
| | | { |
| | | throw new InitializationException(ERR_JEB_ENTRY_CONTAINER_ALREADY_REGISTERED.get( |
| | | ec1.getDatabasePrefix(), baseDN)); |
| | | } |
| | | |
| | | this.entryContainers.put(baseDN, entryContainer); |
| | | } |
| | | |
| | | /** |
| | | * Opens the entry containers for multiple base DNs. |
| | | * |
| | | * @param baseDNs The base DNs of the entry containers to open. |
| | | * @throws StorageRuntimeException If a database error occurs while opening |
| | | * the entry container. |
| | | * @throws InitializationException If an initialization error occurs while |
| | | * opening the entry container. |
| | | * @throws ConfigException If a configuration error occurs while |
| | | * opening the entry container. |
| | | */ |
| | | private void openAndRegisterEntryContainers(Set<DN> baseDNs) |
| | | throws StorageRuntimeException, InitializationException, ConfigException |
| | | { |
| | | EntryID id; |
| | | EntryID highestID = null; |
| | | for(DN baseDN : baseDNs) |
| | | { |
| | | EntryContainer ec = openEntryContainer(baseDN, null); |
| | | id = ec.getHighestEntryID(); |
| | | registerEntryContainer(baseDN, ec); |
| | | if(highestID == null || id.compareTo(highestID) > 0) |
| | | { |
| | | highestID = id; |
| | | } |
| | | } |
| | | |
| | | nextid = new AtomicLong(highestID.longValue() + 1); |
| | | } |
| | | |
| | | /** |
| | | * Unregisters the entry container for a base DN. |
| | | * |
| | | * @param baseDN The base DN of the entry container to close. |
| | | * @return The entry container that was unregistered or NULL if a entry |
| | | * container for the base DN was not registered. |
| | | */ |
| | | public EntryContainer unregisterEntryContainer(DN baseDN) |
| | | { |
| | | return entryContainers.remove(baseDN); |
| | | } |
| | | |
| | | /** |
| | | * Retrieves the compressed schema manager for this backend. |
| | | * |
| | | * @return The compressed schema manager for this backend. |
| | | */ |
| | | public JECompressedSchema getCompressedSchema() |
| | | { |
| | | return compressedSchema; |
| | | } |
| | | |
| | | /** |
| | | * Get the DatabaseEnvironmentMonitor object for JE environment used by this |
| | | * root container. |
| | | * |
| | | * @return The DatabaseEnvironmentMonito object. |
| | | */ |
| | | public DatabaseEnvironmentMonitor getMonitorProvider() |
| | | { |
| | | if(monitor == null) |
| | | { |
| | | String monitorName = backend.getBackendID() + " Database Storage"; |
| | | monitor = new DatabaseEnvironmentMonitor(monitorName, this); |
| | | } |
| | | |
| | | return monitor; |
| | | } |
| | | |
| | | /** |
| | | * Preload the database cache. There is no preload if the configured preload |
| | | * time limit is zero. |
| | | * |
| | | * @param timeLimit The time limit for the preload process. |
| | | */ |
| | | public void preload(long timeLimit) |
| | | { |
| | | if (timeLimit > 0) |
| | | { |
| | | // Get a list of all the databases used by the backend. |
| | | ArrayList<DatabaseContainer> dbList = new ArrayList<DatabaseContainer>(); |
| | | for (EntryContainer ec : entryContainers.values()) |
| | | { |
| | | ec.sharedLock.lock(); |
| | | try |
| | | { |
| | | ec.listDatabases(dbList); |
| | | } |
| | | finally |
| | | { |
| | | ec.sharedLock.unlock(); |
| | | } |
| | | } |
| | | |
| | | // Sort the list in order of priority. |
| | | Collections.sort(dbList, new DbPreloadComparator()); |
| | | |
| | | // Preload each database until we reach the time limit or the cache |
| | | // is filled. |
| | | try |
| | | { |
| | | // Configure preload of Leaf Nodes (LNs) containing the data values. |
| | | PreloadConfig preloadConfig = new PreloadConfig(); |
| | | preloadConfig.setLoadLNs(true); |
| | | |
| | | logger.info(NOTE_JEB_CACHE_PRELOAD_STARTED, backend.getBackendID()); |
| | | |
| | | boolean isInterrupted = false; |
| | | |
| | | long timeEnd = System.currentTimeMillis() + timeLimit; |
| | | |
| | | for (DatabaseContainer db : dbList) |
| | | { |
| | | // Calculate the remaining time. |
| | | long timeRemaining = timeEnd - System.currentTimeMillis(); |
| | | if (timeRemaining <= 0) |
| | | { |
| | | break; |
| | | } |
| | | |
| | | preloadConfig.setMaxMillisecs(timeRemaining); |
| | | PreloadStats preloadStats = db.preload(preloadConfig); |
| | | |
| | | if(logger.isTraceEnabled()) |
| | | { |
| | | logger.trace("file=" + db.getName() + " LNs=" + preloadStats.getNLNsLoaded()); |
| | | } |
| | | |
| | | // Stop if the cache is full or the time limit has been exceeded. |
| | | PreloadStatus preloadStatus = preloadStats.getStatus(); |
| | | if (preloadStatus != PreloadStatus.SUCCESS) |
| | | { |
| | | if (preloadStatus == PreloadStatus.EXCEEDED_TIME) { |
| | | logger.info(NOTE_JEB_CACHE_PRELOAD_INTERRUPTED_BY_TIME, backend.getBackendID(), db.getName()); |
| | | } else if (preloadStatus == PreloadStatus.FILLED_CACHE) { |
| | | logger.info(NOTE_JEB_CACHE_PRELOAD_INTERRUPTED_BY_SIZE, backend.getBackendID(), db.getName()); |
| | | } else { |
| | | logger.info(NOTE_JEB_CACHE_PRELOAD_INTERRUPTED_UNKNOWN, backend.getBackendID(), db.getName()); |
| | | } |
| | | |
| | | isInterrupted = true; |
| | | break; |
| | | } |
| | | |
| | | logger.info(NOTE_JEB_CACHE_DB_PRELOADED, db.getName()); |
| | | } |
| | | |
| | | if (!isInterrupted) { |
| | | logger.info(NOTE_JEB_CACHE_PRELOAD_DONE, backend.getBackendID()); |
| | | } |
| | | |
| | | // Log an informational message about the size of the cache. |
| | | EnvironmentStats stats = storage.getStats(new StatsConfig()); |
| | | long total = stats.getCacheTotalBytes(); |
| | | |
| | | logger.info(NOTE_JEB_CACHE_SIZE_AFTER_PRELOAD, total / (1024 * 1024)); |
| | | } |
| | | catch (StorageRuntimeException e) |
| | | { |
| | | logger.traceException(e); |
| | | |
| | | logger.error(ERR_JEB_CACHE_PRELOAD, backend.getBackendID(), |
| | | stackTraceToSingleLineString(e.getCause() != null ? e.getCause() : e)); |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Closes this root container. |
| | | * |
| | | * @throws StorageRuntimeException If an error occurs while attempting to close |
| | | * the root container. |
| | | */ |
| | | public void close() throws StorageRuntimeException |
| | | { |
| | | for(DN baseDN : entryContainers.keySet()) |
| | | { |
| | | EntryContainer ec = unregisterEntryContainer(baseDN); |
| | | ec.exclusiveLock.lock(); |
| | | try |
| | | { |
| | | ec.close(); |
| | | } |
| | | finally |
| | | { |
| | | ec.exclusiveLock.unlock(); |
| | | } |
| | | } |
| | | |
| | | compressedSchema.close(); |
| | | config.removeLocalDBChangeListener(this); |
| | | |
| | | if (storage != null) |
| | | { |
| | | storage.close(); |
| | | storage = null; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Return all the entry containers in this root container. |
| | | * |
| | | * @return The entry containers in this root container. |
| | | */ |
| | | public Collection<EntryContainer> getEntryContainers() |
| | | { |
| | | return entryContainers.values(); |
| | | } |
| | | |
| | | /** |
| | | * Returns all the baseDNs this root container stores. |
| | | * |
| | | * @return The set of DNs this root container stores. |
| | | */ |
| | | public Set<DN> getBaseDNs() |
| | | { |
| | | return entryContainers.keySet(); |
| | | } |
| | | |
| | | /** |
| | | * Return the entry container for a specific base DN. |
| | | * |
| | | * @param baseDN The base DN of the entry container to retrieve. |
| | | * @return The entry container for the base DN. |
| | | */ |
| | | public EntryContainer getEntryContainer(DN baseDN) |
| | | { |
| | | EntryContainer ec = null; |
| | | DN nodeDN = baseDN; |
| | | |
| | | while (ec == null && nodeDN != null) |
| | | { |
| | | ec = entryContainers.get(nodeDN); |
| | | if (ec == null) |
| | | { |
| | | nodeDN = nodeDN.getParentDNInSuffix(); |
| | | } |
| | | } |
| | | |
| | | return ec; |
| | | } |
| | | |
| | | /** |
| | | * Get the environment stats of the JE environment used in this root |
| | | * container. |
| | | * |
| | | * @param statsConfig The configuration to use for the EnvironmentStats |
| | | * object. |
| | | * @return The environment status of the JE environment. |
| | | * @throws StorageRuntimeException If an error occurs while retrieving the stats |
| | | * object. |
| | | */ |
| | | public EnvironmentStats getEnvironmentStats(StatsConfig statsConfig) |
| | | throws StorageRuntimeException |
| | | { |
| | | return storage.getStats(statsConfig); |
| | | } |
| | | |
| | | /** |
| | | * Get the environment transaction stats of the JE environment used |
| | | * in this root container. |
| | | * |
| | | * @param statsConfig The configuration to use for the EnvironmentStats |
| | | * object. |
| | | * @return The environment status of the JE environment. |
| | | * @throws StorageRuntimeException If an error occurs while retrieving the stats |
| | | * object. |
| | | */ |
| | | public TransactionStats getEnvironmentTransactionStats( |
| | | StatsConfig statsConfig) throws StorageRuntimeException |
| | | { |
| | | return storage.getTransactionStats(statsConfig); |
| | | } |
| | | |
| | | /** |
| | | * Get the environment config of the JE environment used in this root |
| | | * container. |
| | | * |
| | | * @return The environment config of the JE environment. |
| | | * @throws StorageRuntimeException If an error occurs while retrieving the |
| | | * configuration object. |
| | | */ |
| | | public EnvironmentConfig getEnvironmentConfig() throws StorageRuntimeException |
| | | { |
| | | return storage.getConfig(); |
| | | } |
| | | |
| | | /** |
| | | * Get the backend configuration used by this root container. |
| | | * |
| | | * @return The JE backend configuration used by this root container. |
| | | */ |
| | | public LocalDBBackendCfg getConfiguration() |
| | | { |
| | | return config; |
| | | } |
| | | |
| | | /** |
| | | * Get the total number of entries in this root container. |
| | | * |
| | | * @return The number of entries in this root container |
| | | * @throws StorageRuntimeException If an error occurs while retrieving the entry |
| | | * count. |
| | | */ |
| | | public long getEntryCount() throws StorageRuntimeException |
| | | { |
| | | long entryCount = 0; |
| | | for(EntryContainer ec : this.entryContainers.values()) |
| | | { |
| | | ec.sharedLock.lock(); |
| | | try |
| | | { |
| | | entryCount += ec.getEntryCount(); |
| | | } |
| | | finally |
| | | { |
| | | ec.sharedLock.unlock(); |
| | | } |
| | | } |
| | | |
| | | return entryCount; |
| | | } |
| | | |
| | | /** |
| | | * Assign the next entry ID. |
| | | * |
| | | * @return The assigned entry ID. |
| | | */ |
| | | public EntryID getNextEntryID() |
| | | { |
| | | return new EntryID(nextid.getAndIncrement()); |
| | | } |
| | | |
| | | /** |
| | | * Return the lowest entry ID assigned. |
| | | * |
| | | * @return The lowest entry ID assigned. |
| | | */ |
| | | public Long getLowestEntryID() |
| | | { |
| | | return 1L; |
| | | } |
| | | |
| | | /** |
| | | * Resets the next entry ID counter to zero. This should only be used after |
| | | * clearing all databases. |
| | | */ |
| | | public void resetNextEntryID() |
| | | { |
| | | nextid.set(1); |
| | | } |
| | | |
| | | |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public boolean isConfigurationChangeAcceptable( |
| | | LocalDBBackendCfg cfg, |
| | | List<LocalizableMessage> unacceptableReasons) |
| | | { |
| | | boolean acceptable = true; |
| | | |
| | | File parentDirectory = getFileForPath(config.getDBDirectory()); |
| | | File backendDirectory = new File(parentDirectory, config.getBackendId()); |
| | | |
| | | //Make sure the directory either already exists or is able to create. |
| | | if (!backendDirectory.exists()) |
| | | { |
| | | if(!backendDirectory.mkdirs()) |
| | | { |
| | | unacceptableReasons.add(ERR_JEB_CREATE_FAIL.get(backendDirectory.getPath())); |
| | | acceptable = false; |
| | | } |
| | | else |
| | | { |
| | | backendDirectory.delete(); |
| | | } |
| | | } |
| | | //Make sure the directory is valid. |
| | | else if (!backendDirectory.isDirectory()) |
| | | { |
| | | unacceptableReasons.add(ERR_JEB_DIRECTORY_INVALID.get(backendDirectory.getPath())); |
| | | acceptable = false; |
| | | } |
| | | |
| | | try |
| | | { |
| | | FilePermission newBackendPermission = |
| | | FilePermission.decodeUNIXMode(cfg.getDBDirectoryPermissions()); |
| | | |
| | | //Make sure the mode will allow the server itself access to |
| | | //the database |
| | | if(!newBackendPermission.isOwnerWritable() || |
| | | !newBackendPermission.isOwnerReadable() || |
| | | !newBackendPermission.isOwnerExecutable()) |
| | | { |
| | | LocalizableMessage message = ERR_CONFIG_BACKEND_INSANE_MODE.get( |
| | | cfg.getDBDirectoryPermissions()); |
| | | unacceptableReasons.add(message); |
| | | acceptable = false; |
| | | } |
| | | } |
| | | catch(Exception e) |
| | | { |
| | | unacceptableReasons.add(ERR_CONFIG_BACKEND_MODE_INVALID.get(cfg.dn())); |
| | | acceptable = false; |
| | | } |
| | | |
| | | try |
| | | { |
| | | ConfigurableEnvironment.parseConfigEntry(cfg); |
| | | } |
| | | catch (Exception e) |
| | | { |
| | | unacceptableReasons.add(LocalizableMessage.raw(e.getLocalizedMessage())); |
| | | acceptable = false; |
| | | } |
| | | |
| | | return acceptable; |
| | | } |
| | | |
| | | |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public ConfigChangeResult applyConfigurationChange(LocalDBBackendCfg cfg) |
| | | { |
| | | boolean adminActionRequired = false; |
| | | ArrayList<LocalizableMessage> messages = new ArrayList<LocalizableMessage>(); |
| | | |
| | | try |
| | | { |
| | | if(storage != null) |
| | | { |
| | | // Check if any JE non-mutable properties were changed. |
| | | EnvironmentConfig oldEnvConfig = storage.getConfig(); |
| | | EnvironmentConfig newEnvConfig = |
| | | ConfigurableEnvironment.parseConfigEntry(cfg); |
| | | Map<?,?> paramsMap = EnvironmentParams.SUPPORTED_PARAMS; |
| | | |
| | | // Iterate through native JE properties. |
| | | SortedSet<String> jeProperties = cfg.getJEProperty(); |
| | | for (String jeEntry : jeProperties) { |
| | | // There is no need to validate properties yet again. |
| | | StringTokenizer st = new StringTokenizer(jeEntry, "="); |
| | | if (st.countTokens() == 2) { |
| | | String jePropertyName = st.nextToken(); |
| | | String jePropertyValue = st.nextToken(); |
| | | ConfigParam param = (ConfigParam) paramsMap.get(jePropertyName); |
| | | if (!param.isMutable()) { |
| | | String oldValue = oldEnvConfig.getConfigParam(param.getName()); |
| | | if (!oldValue.equalsIgnoreCase(jePropertyValue)) { |
| | | adminActionRequired = true; |
| | | messages.add(INFO_CONFIG_JE_PROPERTY_REQUIRES_RESTART.get(jePropertyName)); |
| | | if(logger.isTraceEnabled()) { |
| | | logger.trace("The change to the following property " + |
| | | "will take effect when the component is restarted: " + |
| | | jePropertyName); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | // Iterate through JE configuration attributes. |
| | | for (Object o : paramsMap.values()) |
| | | { |
| | | ConfigParam param = (ConfigParam) o; |
| | | if (!param.isMutable()) |
| | | { |
| | | String oldValue = oldEnvConfig.getConfigParam(param.getName()); |
| | | String newValue = newEnvConfig.getConfigParam(param.getName()); |
| | | if (!oldValue.equalsIgnoreCase(newValue)) |
| | | { |
| | | adminActionRequired = true; |
| | | String configAttr = ConfigurableEnvironment. |
| | | getAttributeForProperty(param.getName()); |
| | | if (configAttr != null) |
| | | { |
| | | messages.add(NOTE_JEB_CONFIG_ATTR_REQUIRES_RESTART.get(configAttr)); |
| | | } |
| | | else |
| | | { |
| | | messages.add(NOTE_JEB_CONFIG_ATTR_REQUIRES_RESTART.get(param.getName())); |
| | | } |
| | | if(logger.isTraceEnabled()) |
| | | { |
| | | logger.trace("The change to the following property will " + |
| | | "take effect when the backend is restarted: " + |
| | | param.getName()); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | // This takes care of changes to the JE environment for those |
| | | // properties that are mutable at runtime. |
| | | storage.setMutableConfig(newEnvConfig); |
| | | |
| | | logger.trace("JE database configuration: %s", storage.getConfig()); |
| | | } |
| | | |
| | | // Create the directory if it doesn't exist. |
| | | if(!cfg.getDBDirectory().equals(this.config.getDBDirectory())) |
| | | { |
| | | File parentDirectory = getFileForPath(cfg.getDBDirectory()); |
| | | File backendDirectory = |
| | | new File(parentDirectory, cfg.getBackendId()); |
| | | |
| | | if (!backendDirectory.exists()) |
| | | { |
| | | if(!backendDirectory.mkdirs()) |
| | | { |
| | | messages.add(ERR_JEB_CREATE_FAIL.get(backendDirectory.getPath())); |
| | | return new ConfigChangeResult( |
| | | DirectoryServer.getServerErrorResultCode(), |
| | | adminActionRequired, |
| | | messages); |
| | | } |
| | | } |
| | | //Make sure the directory is valid. |
| | | else if (!backendDirectory.isDirectory()) |
| | | { |
| | | messages.add(ERR_JEB_DIRECTORY_INVALID.get(backendDirectory.getPath())); |
| | | return new ConfigChangeResult( |
| | | DirectoryServer.getServerErrorResultCode(), |
| | | adminActionRequired, |
| | | messages); |
| | | } |
| | | |
| | | adminActionRequired = true; |
| | | messages.add(NOTE_JEB_CONFIG_DB_DIR_REQUIRES_RESTART.get( |
| | | this.config.getDBDirectory(), cfg.getDBDirectory())); |
| | | } |
| | | |
| | | if(!cfg.getDBDirectoryPermissions().equalsIgnoreCase( |
| | | config.getDBDirectoryPermissions()) || |
| | | !cfg.getDBDirectory().equals(this.config.getDBDirectory())) |
| | | { |
| | | FilePermission backendPermission; |
| | | try |
| | | { |
| | | backendPermission = |
| | | FilePermission.decodeUNIXMode(cfg.getDBDirectoryPermissions()); |
| | | } |
| | | catch(Exception e) |
| | | { |
| | | messages.add(ERR_CONFIG_BACKEND_MODE_INVALID.get(config.dn())); |
| | | return new ConfigChangeResult( |
| | | DirectoryServer.getServerErrorResultCode(), |
| | | adminActionRequired, |
| | | messages); |
| | | } |
| | | |
| | | //Make sure the mode will allow the server itself access to |
| | | //the database |
| | | if(!backendPermission.isOwnerWritable() || |
| | | !backendPermission.isOwnerReadable() || |
| | | !backendPermission.isOwnerExecutable()) |
| | | { |
| | | messages.add(ERR_CONFIG_BACKEND_INSANE_MODE.get( |
| | | cfg.getDBDirectoryPermissions())); |
| | | return new ConfigChangeResult( |
| | | DirectoryServer.getServerErrorResultCode(), |
| | | adminActionRequired, |
| | | messages); |
| | | } |
| | | |
| | | // Get the backend database backendDirectory permissions and apply |
| | | if(FilePermission.canSetPermissions()) |
| | | { |
| | | File parentDirectory = getFileForPath(config.getDBDirectory()); |
| | | File backendDirectory = new File(parentDirectory, config.getBackendId()); |
| | | try |
| | | { |
| | | if (!FilePermission.setPermissions(backendDirectory, backendPermission)) |
| | | { |
| | | logger.warn(WARN_JEB_UNABLE_SET_PERMISSIONS, backendPermission, backendDirectory); |
| | | } |
| | | } |
| | | catch(Exception e) |
| | | { |
| | | // Log an warning that the permissions were not set. |
| | | logger.warn(WARN_JEB_SET_PERMISSIONS_FAILED, backendDirectory, e); |
| | | } |
| | | } |
| | | } |
| | | |
| | | getMonitorProvider().enableFilterUseStats( |
| | | cfg.isIndexFilterAnalyzerEnabled()); |
| | | getMonitorProvider() |
| | | .setMaxEntries(cfg.getIndexFilterAnalyzerMaxFilters()); |
| | | |
| | | this.config = cfg; |
| | | } |
| | | catch (Exception e) |
| | | { |
| | | messages.add(LocalizableMessage.raw(stackTraceToSingleLineString(e))); |
| | | return new ConfigChangeResult(DirectoryServer.getServerErrorResultCode(), |
| | | adminActionRequired, |
| | | messages); |
| | | } |
| | | |
| | | return new ConfigChangeResult(ResultCode.SUCCESS, adminActionRequired, messages); |
| | | } |
| | | |
| | | /** |
| | | * Returns whether this container JE database environment is |
| | | * open, valid and can be used. |
| | | * |
| | | * @return {@code true} if valid, or {@code false} otherwise. |
| | | */ |
| | | public boolean isValid() { |
| | | return storage.isValid(); |
| | | } |
| | | } |
| New file |
| | |
| | | /* |
| | | * CDDL HEADER START |
| | | * |
| | | * The contents of this file are subject to the terms of the |
| | | * Common Development and Distribution License, Version 1.0 only |
| | | * (the "License"). You may not use this file except in compliance |
| | | * with the License. |
| | | * |
| | | * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt |
| | | * or http://forgerock.org/license/CDDLv1.0.html. |
| | | * See the License for the specific language governing permissions |
| | | * and limitations under the License. |
| | | * |
| | | * When distributing Covered Code, include this CDDL HEADER in each |
| | | * file and include the License file at legal-notices/CDDLv1_0.txt. |
| | | * If applicable, add the following below this CDDL HEADER, with the |
| | | * fields enclosed by brackets "[]" replaced with your own identifying |
| | | * information: |
| | | * Portions Copyright [yyyy] [name of copyright owner] |
| | | * |
| | | * CDDL HEADER END |
| | | * |
| | | * |
| | | * Copyright 2008 Sun Microsystems, Inc. |
| | | * Portions Copyright 2014 ForgeRock AS |
| | | */ |
| | | package org.opends.server.backends.pluggable; |
| | | |
| | | import java.util.List; |
| | | |
| | | import org.forgerock.opendj.ldap.ByteString; |
| | | import org.opends.server.types.Attribute; |
| | | import org.opends.server.types.AttributeType; |
| | | import org.opends.server.types.Entry; |
| | | import org.opends.server.types.SortKey; |
| | | import org.opends.server.types.SortOrder; |
| | | |
| | | /** |
| | | * This class defines a data structure that holds a set of attribute values that |
| | | * are associated with a sort order for a given entry. Any or all of the |
| | | * attribute values may be {@code null} if the entry does not include any values |
| | | * for the attribute type targeted by the corresponding sort key. |
| | | * <BR><BR> |
| | | * This class implements the {@code Comparable} interface and may therefore be |
| | | * used to order the elements in components like {@code TreeMap} and |
| | | * {@code TreeSet}. |
| | | * <p> |
| | | * FIXME: replace with the SDK's SortKey? |
| | | */ |
| | | public class SortValues |
| | | implements Comparable<SortValues> |
| | | { |
| | | /** The set of sort keys (attribute values) in this sort order. */ |
| | | private ByteString[] values; |
| | | /** |
| | | * The types of sort keys. |
| | | * |
| | | * @see #values |
| | | */ |
| | | private AttributeType[] types; |
| | | |
| | | /** The entry ID for the entry associated with this sort values. */ |
| | | private EntryID entryID; |
| | | |
| | | /** The sort order for this set of sort values. */ |
| | | private SortOrder sortOrder; |
| | | |
| | | |
| | | |
| | | /** |
| | | * Creates a new sort values object with the provided information. |
| | | * |
| | | * @param entryID The entry ID for the entry associated with this set of |
| | | * values. |
| | | * @param values The attribute values for this sort values. |
| | | * @param sortOrder The sort order to use to obtain the necessary values. |
| | | */ |
| | | public SortValues(EntryID entryID, ByteString[] values, |
| | | SortOrder sortOrder) |
| | | { |
| | | this.entryID = entryID; |
| | | this.sortOrder = sortOrder; |
| | | this.values = values; |
| | | |
| | | final SortKey[] sortKeys = sortOrder.getSortKeys(); |
| | | this.types = new AttributeType[sortKeys.length]; |
| | | for (int i = 0; i < sortKeys.length; i++) |
| | | { |
| | | types[i] = sortKeys[i].getAttributeType(); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Creates a new sort values object with the provided information. |
| | | * |
| | | * @param entryID The entry ID for the entry associated with this set of |
| | | * values. |
| | | * @param entry The entry containing the values to extract and use when |
| | | * sorting. |
| | | * @param sortOrder The sort order to use to obtain the necessary values. |
| | | */ |
| | | public SortValues(EntryID entryID, Entry entry, SortOrder sortOrder) |
| | | { |
| | | this.entryID = entryID; |
| | | this.sortOrder = sortOrder; |
| | | |
| | | SortKey[] sortKeys = sortOrder.getSortKeys(); |
| | | this.values = new ByteString[sortKeys.length]; |
| | | this.types = new AttributeType[sortKeys.length]; |
| | | for (int i=0; i < sortKeys.length; i++) |
| | | { |
| | | SortKey sortKey = sortKeys[i]; |
| | | types[i] = sortKey.getAttributeType(); |
| | | List<Attribute> attrList = entry.getAttribute(types[i]); |
| | | if (attrList != null) |
| | | { |
| | | values[i] = findBestMatchingValue(sortKey, attrList); |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Finds the best matching attribute value for the provided sort key in the |
| | | * provided attribute list. |
| | | * <p> |
| | | * There may be multiple versions of this attribute in the target entry (e.g., |
| | | * with different sets of options), and it may also be a multivalued |
| | | * attribute. In that case, we need to find the value that is the best match |
| | | * for the corresponding sort key (i.e., for sorting in ascending order, we |
| | | * want to find the lowest value; for sorting in descending order, we want to |
| | | * find the highest value). This is handled by the SortKey.compareValues |
| | | * method. |
| | | */ |
| | | private ByteString findBestMatchingValue(SortKey sortKey, List<Attribute> attrList) |
| | | { |
| | | ByteString sortValue = null; |
| | | for (Attribute a : attrList) |
| | | { |
| | | for (ByteString v : a) |
| | | { |
| | | if (sortValue == null || sortKey.compareValues(v, sortValue) < 0) |
| | | { |
| | | sortValue = v; |
| | | } |
| | | } |
| | | } |
| | | return sortValue; |
| | | } |
| | | |
| | | /** |
| | | * Compares this set of sort values with the provided set of values to |
| | | * determine their relative order in a sorted list. |
| | | * |
| | | * @param sortValues The set of values to compare against this sort values. |
| | | * It must also have the same sort order as this set of |
| | | * values. |
| | | * |
| | | * @return A negative value if this sort values object should come before the |
| | | * provided values in a sorted list, a positive value if this sort |
| | | * values object should come after the provided values in a sorted |
| | | * list, or zero if there is no significant difference in their |
| | | * relative order. |
| | | */ |
| | | @Override |
| | | public int compareTo(SortValues sortValues) |
| | | { |
| | | SortKey[] sortKeys = sortOrder.getSortKeys(); |
| | | |
| | | for (int i=0; i < values.length; i++) |
| | | { |
| | | int compareValue = sortKeys[i].compareValues(values[i], sortValues.values[i]); |
| | | if (compareValue != 0) |
| | | { |
| | | return compareValue; |
| | | } |
| | | } |
| | | |
| | | // If we've gotten here, then we can't tell a difference between the sets of |
| | | // sort values, so sort based on entry ID. |
| | | return entryID.compareTo(sortValues.entryID); |
| | | } |
| | | |
| | | /** |
| | | * Compares the first element in this set of sort values with the provided |
| | | * assertion value to determine whether the assertion value is greater than or |
| | | * equal to the initial sort value. This is used during VLV processing to |
| | | * find the offset by assertion value. |
| | | * |
| | | * @param assertionValue The assertion value to compare against the first |
| | | * sort value. |
| | | * |
| | | * @return A negative value if the provided assertion value should come |
| | | * before the first sort value, zero if the provided assertion value |
| | | * is equal to the first sort value, or a positive value if the |
| | | * provided assertion value should come after the first sort value. |
| | | */ |
| | | public int compareTo(ByteString assertionValue) |
| | | { |
| | | SortKey sortKey = sortOrder.getSortKeys()[0]; |
| | | return sortKey.compareValues(values[0], assertionValue); |
| | | } |
| | | |
| | | /** |
| | | * Retrieves a string representation of this sort values object. |
| | | * |
| | | * @return A string representation of this sort values object. |
| | | */ |
| | | @Override |
| | | public String toString() |
| | | { |
| | | StringBuilder buffer = new StringBuilder(); |
| | | toString(buffer); |
| | | return buffer.toString(); |
| | | } |
| | | |
| | | /** |
| | | * Appends a string representation of this sort values object to the provided |
| | | * buffer. |
| | | * |
| | | * @param buffer The buffer to which the information should be appended. |
| | | */ |
| | | public void toString(StringBuilder buffer) |
| | | { |
| | | buffer.append("SortValues("); |
| | | |
| | | SortKey[] sortKeys = sortOrder.getSortKeys(); |
| | | for (int i=0; i < sortKeys.length; i++) |
| | | { |
| | | if (i > 0) |
| | | { |
| | | buffer.append(","); |
| | | } |
| | | |
| | | buffer.append(sortKeys[i].ascending() ? "+" : "-"); |
| | | |
| | | buffer.append(sortKeys[i].getAttributeType().getNameOrOID()); |
| | | buffer.append("="); |
| | | buffer.append(values[i]); |
| | | } |
| | | |
| | | buffer.append(", id="); |
| | | buffer.append(entryID); |
| | | buffer.append(")"); |
| | | } |
| | | |
| | | /** |
| | | * Retrieve the attribute values in this sort values. |
| | | * |
| | | * @return The array of attribute values for this sort values. |
| | | */ |
| | | public ByteString[] getValues() |
| | | { |
| | | return values; |
| | | } |
| | | |
| | | /** |
| | | * Retrieve the type of the attribute values in this sort values. |
| | | * |
| | | * @return The array of type of the attribute values for this sort values. |
| | | */ |
| | | public AttributeType[] getTypes() |
| | | { |
| | | return types; |
| | | } |
| | | |
| | | /** |
| | | * Retrieve the entry ID in this sort values. |
| | | * |
| | | * @return The entry ID for this sort values. |
| | | */ |
| | | public long getEntryID() |
| | | { |
| | | return entryID.longValue(); |
| | | } |
| | | } |
| New file |
| | |
| | | /* |
| | | * CDDL HEADER START |
| | | * |
| | | * The contents of this file are subject to the terms of the |
| | | * Common Development and Distribution License, Version 1.0 only |
| | | * (the "License"). You may not use this file except in compliance |
| | | * with the License. |
| | | * |
| | | * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt |
| | | * or http://forgerock.org/license/CDDLv1.0.html. |
| | | * See the License for the specific language governing permissions |
| | | * and limitations under the License. |
| | | * |
| | | * When distributing Covered Code, include this CDDL HEADER in each |
| | | * file and include the License file at legal-notices/CDDLv1_0.txt. |
| | | * If applicable, add the following below this CDDL HEADER, with the |
| | | * fields enclosed by brackets "[]" replaced with your own identifying |
| | | * information: |
| | | * Portions Copyright [yyyy] [name of copyright owner] |
| | | * |
| | | * CDDL HEADER END |
| | | * |
| | | * |
| | | * Copyright 2006-2008 Sun Microsystems, Inc. |
| | | * Portions Copyright 2014 ForgeRock AS |
| | | */ |
| | | package org.opends.server.backends.pluggable; |
| | | |
| | | import org.forgerock.opendj.ldap.ByteString; |
| | | import org.forgerock.opendj.ldap.ByteStringBuilder; |
| | | import org.forgerock.opendj.ldap.DecodeException; |
| | | import org.forgerock.opendj.ldap.ResultCode; |
| | | import org.forgerock.opendj.ldap.schema.MatchingRule; |
| | | import org.opends.server.backends.pluggable.BackendImpl.StorageRuntimeException; |
| | | import org.opends.server.types.AttributeType; |
| | | import org.opends.server.types.DirectoryException; |
| | | import org.opends.server.types.SortKey; |
| | | |
| | | import static org.opends.server.backends.pluggable.JebFormat.*; |
| | | |
| | | |
| | | |
| | | /** |
| | | * This class represents a partial sorted set of sorted entries in a VLV |
| | | * index. |
| | | */ |
| | | public class SortValuesSet |
| | | { |
| | | private long[] entryIDs; |
| | | |
| | | private int[] valuesBytesOffsets; |
| | | private byte[] valuesBytes; |
| | | |
| | | private ByteString key; |
| | | |
| | | private VLVIndex vlvIndex; |
| | | |
| | | /** |
| | | * Construct an empty sort values set with the given information. |
| | | * |
| | | * @param vlvIndex The VLV index using this set. |
| | | */ |
| | | public SortValuesSet(VLVIndex vlvIndex) |
| | | { |
| | | this.key = ByteString.empty(); |
| | | this.entryIDs = null; |
| | | this.valuesBytes = null; |
| | | this.valuesBytesOffsets = null; |
| | | this.vlvIndex = vlvIndex; |
| | | } |
| | | |
| | | /** |
| | | * Construct a sort values set from the database. |
| | | * |
| | | * @param key The database key used to locate this set. |
| | | * @param value The bytes to decode and construct this set. |
| | | * @param vlvIndex The VLV index using this set. |
| | | */ |
| | | public SortValuesSet(ByteString key, ByteString value, VLVIndex vlvIndex) |
| | | { |
| | | this.key = key; |
| | | this.vlvIndex = vlvIndex; |
| | | if(value == null) |
| | | { |
| | | entryIDs = new long[0]; |
| | | return; |
| | | } |
| | | |
| | | entryIDs = getEncodedIDs(value, 0); |
| | | int valuesBytesOffset = entryIDs.length * 8 + 4; |
| | | int valuesBytesLength = value.length() - valuesBytesOffset; |
| | | valuesBytes = new byte[valuesBytesLength]; |
| | | System.arraycopy(value, valuesBytesOffset, valuesBytes, 0, |
| | | valuesBytesLength); |
| | | this.valuesBytesOffsets = null; |
| | | } |
| | | |
| | | private SortValuesSet() |
| | | {} |
| | | |
| | | /** |
| | | * Add the given entryID and values from this VLV index. |
| | | * |
| | | * @param entryID The entry ID to add. |
| | | * @param values The values to add. |
| | | * @param types The types of the values to add. |
| | | * @return True if the information was successfully added or False |
| | | * otherwise. |
| | | * @throws DirectoryException If a Directory Server error occurs. |
| | | * @throws StorageRuntimeException If an error occurs in the JE database. |
| | | */ |
| | | public boolean add(long entryID, ByteString[] values, AttributeType[] types) |
| | | throws StorageRuntimeException, DirectoryException |
| | | { |
| | | if(values == null) |
| | | { |
| | | return false; |
| | | } |
| | | |
| | | if(entryIDs == null || entryIDs.length == 0) |
| | | { |
| | | entryIDs = new long[] { entryID }; |
| | | valuesBytes = attributeValuesToDatabase(values, types); |
| | | if(valuesBytesOffsets != null) |
| | | { |
| | | valuesBytesOffsets = new int[] { 0 }; |
| | | } |
| | | return true; |
| | | } |
| | | if (vlvIndex.comparator.compare( |
| | | this, entryIDs.length - 1, entryID, values) < 0) |
| | | { |
| | | long[] updatedEntryIDs = new long[entryIDs.length + 1]; |
| | | System.arraycopy(entryIDs, 0, updatedEntryIDs, 0, entryIDs.length); |
| | | updatedEntryIDs[entryIDs.length] = entryID; |
| | | |
| | | byte[] newValuesBytes = attributeValuesToDatabase(values, types); |
| | | byte[] updatedValuesBytes = new byte[valuesBytes.length + |
| | | newValuesBytes.length]; |
| | | System.arraycopy(valuesBytes, 0, updatedValuesBytes, 0, |
| | | valuesBytes.length); |
| | | System.arraycopy(newValuesBytes, 0, updatedValuesBytes, |
| | | valuesBytes.length, |
| | | newValuesBytes.length); |
| | | |
| | | if(valuesBytesOffsets != null) |
| | | { |
| | | int[] updatedValuesBytesOffsets = |
| | | new int[valuesBytesOffsets.length + 1]; |
| | | System.arraycopy(valuesBytesOffsets, 0, updatedValuesBytesOffsets, |
| | | 0, valuesBytesOffsets.length); |
| | | updatedValuesBytesOffsets[valuesBytesOffsets.length] = |
| | | updatedValuesBytes.length - newValuesBytes.length; |
| | | valuesBytesOffsets = updatedValuesBytesOffsets; |
| | | } |
| | | |
| | | entryIDs = updatedEntryIDs; |
| | | valuesBytes = updatedValuesBytes; |
| | | return true; |
| | | } |
| | | else |
| | | { |
| | | int pos = binarySearch(entryID, values); |
| | | if(pos >= 0) |
| | | { |
| | | if(entryIDs[pos] == entryID) |
| | | { |
| | | // The entry ID is alreadly present. |
| | | return false; |
| | | } |
| | | } |
| | | else |
| | | { |
| | | // For a negative return value r, the vlvIndex -(r+1) gives the array |
| | | // ndex at which the specified value can be inserted to maintain |
| | | // the sorted order of the array. |
| | | pos = -(pos+1); |
| | | } |
| | | |
| | | long[] updatedEntryIDs = new long[entryIDs.length + 1]; |
| | | System.arraycopy(entryIDs, 0, updatedEntryIDs, 0, pos); |
| | | System.arraycopy(entryIDs, pos, updatedEntryIDs, pos+1, |
| | | entryIDs.length-pos); |
| | | updatedEntryIDs[pos] = entryID; |
| | | |
| | | byte[] newValuesBytes = attributeValuesToDatabase(values, types); |
| | | // BUG valuesBytesOffsets might be null ? If not why testing below ? |
| | | int valuesPos = valuesBytesOffsets[pos]; |
| | | byte[] updatedValuesBytes = new byte[valuesBytes.length + |
| | | newValuesBytes.length]; |
| | | System.arraycopy(valuesBytes, 0, updatedValuesBytes, 0, valuesPos); |
| | | System.arraycopy(valuesBytes, valuesPos, updatedValuesBytes, |
| | | valuesPos + newValuesBytes.length, |
| | | valuesBytes.length - valuesPos); |
| | | System.arraycopy(newValuesBytes, 0, updatedValuesBytes, valuesPos, |
| | | newValuesBytes.length); |
| | | |
| | | if(valuesBytesOffsets != null) |
| | | { |
| | | int[] updatedValuesBytesOffsets = |
| | | new int[valuesBytesOffsets.length + 1]; |
| | | System.arraycopy(valuesBytesOffsets, 0, updatedValuesBytesOffsets, |
| | | 0, pos); |
| | | // Update the rest of the offsets one by one - Expensive! |
| | | for(int i = pos; i < valuesBytesOffsets.length; i++) |
| | | { |
| | | updatedValuesBytesOffsets[i+1] = |
| | | valuesBytesOffsets[i] + newValuesBytes.length; |
| | | } |
| | | updatedValuesBytesOffsets[pos] = valuesBytesOffsets[pos]; |
| | | valuesBytesOffsets = updatedValuesBytesOffsets; |
| | | } |
| | | |
| | | entryIDs = updatedEntryIDs; |
| | | valuesBytes = updatedValuesBytes; |
| | | } |
| | | |
| | | return true; |
| | | } |
| | | |
| | | /** |
| | | * Remove the given entryID and values from this VLV index. |
| | | * |
| | | * @param entryID The entry ID to remove. |
| | | * @param values The values to remove. |
| | | * @return True if the information was successfully removed or False |
| | | * otherwise. |
| | | * @throws DirectoryException If a Directory Server error occurs. |
| | | * @throws StorageRuntimeException If an error occurs in the JE database. |
| | | */ |
| | | public boolean remove(long entryID, ByteString[] values) |
| | | throws StorageRuntimeException, DirectoryException |
| | | { |
| | | if(entryIDs == null || entryIDs.length == 0) |
| | | { |
| | | return false; |
| | | } |
| | | |
| | | if(valuesBytesOffsets == null) |
| | | { |
| | | updateValuesBytesOffsets(); |
| | | } |
| | | |
| | | int pos = binarySearch(entryID, values); |
| | | if(pos < 0) |
| | | { |
| | | // Not found. |
| | | return false; |
| | | } |
| | | else |
| | | { |
| | | // Found it. |
| | | long[] updatedEntryIDs = new long[entryIDs.length - 1]; |
| | | System.arraycopy(entryIDs, 0, updatedEntryIDs, 0, pos); |
| | | System.arraycopy(entryIDs, pos+1, updatedEntryIDs, pos, |
| | | entryIDs.length-pos-1); |
| | | int valuesLength; |
| | | int valuesPos = valuesBytesOffsets[pos]; |
| | | if(pos < valuesBytesOffsets.length - 1) |
| | | { |
| | | valuesLength = valuesBytesOffsets[pos+1] - valuesPos; |
| | | } |
| | | else |
| | | { |
| | | valuesLength = valuesBytes.length - valuesPos; |
| | | } |
| | | byte[] updatedValuesBytes = new byte[valuesBytes.length - valuesLength]; |
| | | System.arraycopy(valuesBytes, 0, updatedValuesBytes, 0, valuesPos); |
| | | System.arraycopy(valuesBytes, valuesPos + valuesLength, |
| | | updatedValuesBytes, valuesPos, |
| | | valuesBytes.length - valuesPos - valuesLength); |
| | | |
| | | int[] updatedValuesBytesOffsets = new int[valuesBytesOffsets.length - 1]; |
| | | System.arraycopy(valuesBytesOffsets, 0, updatedValuesBytesOffsets, |
| | | 0, pos); |
| | | // Update the rest of the offsets one by one - Expensive! |
| | | for(int i = pos + 1; i < valuesBytesOffsets.length; i++) |
| | | { |
| | | updatedValuesBytesOffsets[i-1] = |
| | | valuesBytesOffsets[i] - valuesLength; |
| | | } |
| | | |
| | | entryIDs = updatedEntryIDs; |
| | | valuesBytes = updatedValuesBytes; |
| | | valuesBytesOffsets = updatedValuesBytesOffsets; |
| | | return true; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Split portions of this set into another set. The values of the new set is |
| | | * from the end of this set. |
| | | * |
| | | * @param splitLength The size of the new set. |
| | | * @return The split set. |
| | | */ |
| | | public SortValuesSet split(int splitLength) |
| | | { |
| | | if(valuesBytesOffsets == null) |
| | | { |
| | | updateValuesBytesOffsets(); |
| | | } |
| | | |
| | | long[] splitEntryIDs = new long[splitLength]; |
| | | byte[] splitValuesBytes = new byte[valuesBytes.length - |
| | | valuesBytesOffsets[valuesBytesOffsets.length - splitLength]]; |
| | | int[] splitValuesBytesOffsets = new int[splitLength]; |
| | | |
| | | long[] updatedEntryIDs = new long[entryIDs.length - splitEntryIDs.length]; |
| | | System.arraycopy(entryIDs, 0, updatedEntryIDs, 0, updatedEntryIDs.length); |
| | | System.arraycopy(entryIDs, updatedEntryIDs.length, splitEntryIDs, 0, |
| | | splitEntryIDs.length); |
| | | |
| | | byte[] updatedValuesBytes = |
| | | new byte[valuesBytesOffsets[valuesBytesOffsets.length - splitLength]]; |
| | | System.arraycopy(valuesBytes, 0, updatedValuesBytes, 0, |
| | | updatedValuesBytes.length); |
| | | System.arraycopy(valuesBytes, updatedValuesBytes.length, splitValuesBytes, |
| | | 0, splitValuesBytes.length); |
| | | |
| | | int[] updatedValuesBytesOffsets = |
| | | new int[valuesBytesOffsets.length - splitValuesBytesOffsets.length]; |
| | | System.arraycopy(valuesBytesOffsets, 0, updatedValuesBytesOffsets, |
| | | 0, updatedValuesBytesOffsets.length); |
| | | for(int i = updatedValuesBytesOffsets.length; |
| | | i < valuesBytesOffsets.length; i++) |
| | | { |
| | | splitValuesBytesOffsets[i - updatedValuesBytesOffsets.length] = |
| | | valuesBytesOffsets[i] - |
| | | valuesBytesOffsets[updatedValuesBytesOffsets.length]; |
| | | } |
| | | |
| | | SortValuesSet splitValuesSet = new SortValuesSet(); |
| | | |
| | | splitValuesSet.entryIDs = splitEntryIDs; |
| | | splitValuesSet.key = this.key; |
| | | splitValuesSet.valuesBytes = splitValuesBytes; |
| | | splitValuesSet.valuesBytesOffsets = splitValuesBytesOffsets; |
| | | splitValuesSet.vlvIndex = this.vlvIndex; |
| | | |
| | | entryIDs = updatedEntryIDs; |
| | | valuesBytes = updatedValuesBytes; |
| | | valuesBytesOffsets = updatedValuesBytesOffsets; |
| | | key = null; |
| | | |
| | | return splitValuesSet; |
| | | } |
| | | |
| | | /** |
| | | * Encode this set to its database format. |
| | | * |
| | | * @return The encoded bytes representing this set or null if |
| | | * this set is empty. |
| | | */ |
| | | public ByteString toByteString() |
| | | { |
| | | if(size() == 0) |
| | | { |
| | | return null; |
| | | } |
| | | |
| | | byte[] entryIDBytes = JebFormat.entryIDListToDatabase(entryIDs); |
| | | byte[] concatBytes = new byte[entryIDBytes.length + valuesBytes.length + 4]; |
| | | int v = entryIDs.length; |
| | | |
| | | for (int j = 3; j >= 0; j--) |
| | | { |
| | | concatBytes[j] = (byte) (v & 0xFF); |
| | | v >>>= 8; |
| | | } |
| | | |
| | | System.arraycopy(entryIDBytes, 0, concatBytes, 4, entryIDBytes.length); |
| | | System.arraycopy(valuesBytes, 0, concatBytes, entryIDBytes.length+4, |
| | | valuesBytes.length); |
| | | |
| | | return ByteString.valueOf(concatBytes); |
| | | } |
| | | |
| | | /** |
| | | * Get the size of the provided encoded set. |
| | | * |
| | | * @param bytes The encoded bytes of a SortValuesSet to decode the size from. |
| | | * @param offset The byte offset to start decoding. |
| | | * @return The size of the provided encoded set. |
| | | */ |
| | | public static int getEncodedSize(ByteString bytes, int offset) |
| | | { |
| | | int v = 0; |
| | | for (int i = offset; i < offset + 4; i++) |
| | | { |
| | | v <<= 8; |
| | | v |= (bytes.byteAt(i) & 0xFF); |
| | | } |
| | | return v; |
| | | } |
| | | |
| | | /** |
| | | * Get the IDs from the provided encoded set. |
| | | * |
| | | * @param bytes The encoded bytes of a SortValuesSet to decode the IDs from. |
| | | * @param offset The byte offset to start decoding. |
| | | * @return The decoded IDs in the provided encoded set. |
| | | */ |
| | | public static long[] getEncodedIDs(ByteString bytes, int offset) |
| | | { |
| | | int length = getEncodedSize(bytes, offset) * 8; |
| | | int offset2 = offset + 4; |
| | | ByteString entryIDBytes = bytes.subSequence(offset2, offset2 + length); |
| | | return JebFormat.entryIDListFromDatabase(entryIDBytes); |
| | | } |
| | | |
| | | /** |
| | | * Searches this set for the specified values and entry ID using the binary |
| | | * search algorithm. |
| | | * |
| | | * @param entryID The entry ID to match or -1 if not matching on entry ID. |
| | | * @param values The values to match. |
| | | * @return Index of the entry matching the values and optionally the entry ID |
| | | * if it is found or a negative index if its not found. |
| | | * @throws DirectoryException If a Directory Server error occurs. |
| | | * @throws StorageRuntimeException If an error occurs in the JE database. |
| | | */ |
| | | int binarySearch(long entryID, ByteString... values) |
| | | throws StorageRuntimeException, DirectoryException |
| | | { |
| | | if(entryIDs == null || entryIDs.length == 0) |
| | | { |
| | | return -1; |
| | | } |
| | | |
| | | int i = 0; |
| | | for(int j = entryIDs.length - 1; i <= j;) |
| | | { |
| | | int k = i + j >> 1; |
| | | int l = vlvIndex.comparator.compare(this, k, entryID, values); |
| | | if (l < 0) |
| | | { |
| | | i = k + 1; |
| | | } |
| | | else if (l > 0) |
| | | { |
| | | j = k - 1; |
| | | } |
| | | else |
| | | { |
| | | return k; |
| | | } |
| | | } |
| | | |
| | | return -(i + 1); |
| | | } |
| | | |
| | | /** |
| | | * Retrieve the size of this set. |
| | | * |
| | | * @return The size of this set. |
| | | */ |
| | | public int size() |
| | | { |
| | | if(entryIDs == null) |
| | | { |
| | | return 0; |
| | | } |
| | | |
| | | return entryIDs.length; |
| | | } |
| | | |
| | | /** |
| | | * Retrieve the entry IDs in this set. |
| | | * |
| | | * @return The entry IDs in this set. |
| | | */ |
| | | public long[] getEntryIDs() |
| | | { |
| | | return entryIDs; |
| | | } |
| | | |
| | | private byte[] attributeValuesToDatabase(ByteString[] values, |
| | | AttributeType[] types) throws DirectoryException |
| | | { |
| | | try |
| | | { |
| | | final ByteStringBuilder builder = new ByteStringBuilder(); |
| | | |
| | | for (int i = 0; i < values.length; i++) |
| | | { |
| | | final ByteString v = values[i]; |
| | | if (v == null) |
| | | { |
| | | builder.appendBERLength(0); |
| | | } |
| | | else |
| | | { |
| | | final MatchingRule eqRule = types[i].getEqualityMatchingRule(); |
| | | final ByteString nv = eqRule.normalizeAttributeValue(v); |
| | | builder.appendBERLength(nv.length()); |
| | | builder.append(nv); |
| | | } |
| | | } |
| | | builder.trimToSize(); |
| | | |
| | | return builder.getBackingArray(); |
| | | } |
| | | catch (DecodeException e) |
| | | { |
| | | throw new DirectoryException( |
| | | ResultCode.INVALID_ATTRIBUTE_SYNTAX, e.getMessageObject(), e); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Returns the key to use for this set of sort values in the database. |
| | | * |
| | | * @return The key as an array of bytes that should be used for this set in |
| | | * the database or NULL if this set is empty. |
| | | * @throws DirectoryException If a Directory Server error occurs. |
| | | * @throws StorageRuntimeException If an error occurs in the JE database. |
| | | */ |
| | | public ByteString getKeyBytes() |
| | | throws StorageRuntimeException, DirectoryException |
| | | { |
| | | if(entryIDs == null || entryIDs.length == 0) |
| | | { |
| | | return null; |
| | | } |
| | | |
| | | if(key != null) |
| | | { |
| | | return key; |
| | | } |
| | | |
| | | if(valuesBytesOffsets == null) |
| | | { |
| | | updateValuesBytesOffsets(); |
| | | } |
| | | |
| | | int vBytesPos = valuesBytesOffsets[valuesBytesOffsets.length - 1]; |
| | | int vBytesLength = valuesBytes.length - vBytesPos; |
| | | |
| | | ByteString idBytes = entryIDToDatabase(entryIDs[entryIDs.length - 1]); |
| | | ByteStringBuilder keyBytes = new ByteStringBuilder(vBytesLength + idBytes.length()); |
| | | keyBytes.append(valuesBytes, vBytesPos, vBytesLength); |
| | | keyBytes.append(idBytes); |
| | | |
| | | key = keyBytes.toByteString(); |
| | | return key; |
| | | } |
| | | |
| | | /** |
| | | * Returns the key to use for this set of sort values in the database. |
| | | * |
| | | * @return The key as a sort values object that should be used for this set in |
| | | * the database or NULL if this set is empty or unbounded. |
| | | * @throws DirectoryException If a Directory Server error occurs. |
| | | * @throws StorageRuntimeException If an error occurs in the JE database. |
| | | */ |
| | | public SortValues getKeySortValues() |
| | | throws StorageRuntimeException, DirectoryException |
| | | { |
| | | if(entryIDs == null || entryIDs.length == 0) |
| | | { |
| | | return null; |
| | | } |
| | | |
| | | if(key != null && key.length() == 0) |
| | | { |
| | | return null; |
| | | } |
| | | |
| | | EntryID id = new EntryID(entryIDs[entryIDs.length - 1]); |
| | | SortKey[] sortKeys = vlvIndex.sortOrder.getSortKeys(); |
| | | int numValues = sortKeys.length; |
| | | ByteString[] values = new ByteString[numValues]; |
| | | for (int i = (entryIDs.length - 1) * numValues, j = 0; |
| | | i < entryIDs.length * numValues; |
| | | i++, j++) |
| | | { |
| | | values[j] = getValue(i); |
| | | } |
| | | |
| | | return new SortValues(id, values, vlvIndex.sortOrder); |
| | | } |
| | | |
| | | /** |
| | | * Returns the sort values at the index in this set. |
| | | * |
| | | * @param index The index of the sort values to get. |
| | | * @return The sort values object at the specified index. |
| | | * @throws DirectoryException If a Directory Server error occurs. |
| | | * @throws StorageRuntimeException If an error occurs in the JE database. |
| | | * @throws JebException If an error occurs in the JE database. |
| | | **/ |
| | | public SortValues getSortValues(int index) |
| | | throws JebException, StorageRuntimeException, DirectoryException |
| | | { |
| | | if(entryIDs == null || entryIDs.length == 0) |
| | | { |
| | | return null; |
| | | } |
| | | |
| | | EntryID id = new EntryID(entryIDs[index]); |
| | | SortKey[] sortKeys = vlvIndex.sortOrder.getSortKeys(); |
| | | int numValues = sortKeys.length; |
| | | ByteString[] values = new ByteString[numValues]; |
| | | for (int i = index * numValues, j = 0; |
| | | i < (index + 1) * numValues; |
| | | i++, j++) |
| | | { |
| | | values[j] = getValue(i); |
| | | } |
| | | |
| | | return new SortValues(id, values, vlvIndex.sortOrder); |
| | | } |
| | | |
| | | private void updateValuesBytesOffsets() |
| | | { |
| | | valuesBytesOffsets = new int[entryIDs.length]; |
| | | int vBytesPos = 0; |
| | | int numAttributes = vlvIndex.sortOrder.getSortKeys().length; |
| | | |
| | | for(int pos = 0; pos < entryIDs.length; pos++) |
| | | { |
| | | valuesBytesOffsets[pos] = vBytesPos; |
| | | |
| | | for(int i = 0; i < numAttributes; i++) |
| | | { |
| | | int valueLength = valuesBytes[vBytesPos] & 0x7F; |
| | | if (valueLength != valuesBytes[vBytesPos++]) |
| | | { |
| | | int valueLengthBytes = valueLength; |
| | | valueLength = 0; |
| | | for (int j=0; j < valueLengthBytes; j++, vBytesPos++) |
| | | { |
| | | valueLength = (valueLength << 8) | (valuesBytes[vBytesPos] & 0xFF); |
| | | } |
| | | } |
| | | |
| | | vBytesPos += valueLength; |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Retrieve an attribute value from this values set. The index is the |
| | | * absolute index. (ie. for a sort on 3 attributes per entry, an vlvIndex of 6 |
| | | * will be the 1st attribute value of the 3rd entry). |
| | | * |
| | | * @param index The vlvIndex of the attribute value to retrieve. |
| | | * @return The byte array representation of the attribute value. |
| | | * @throws DirectoryException If a Directory Server error occurs. |
| | | * @throws StorageRuntimeException If an error occurs in the JE database. |
| | | */ |
| | | public ByteString getValue(int index) |
| | | throws StorageRuntimeException, DirectoryException |
| | | { |
| | | if(valuesBytesOffsets == null) |
| | | { |
| | | updateValuesBytesOffsets(); |
| | | } |
| | | int numAttributes = vlvIndex.sortOrder.getSortKeys().length; |
| | | int vIndex = index / numAttributes; |
| | | int vOffset = index % numAttributes; |
| | | int vBytesPos = valuesBytesOffsets[vIndex]; |
| | | |
| | | // Find the desired value in the sort order set. |
| | | for(int i = 0; i <= vOffset; i++) |
| | | { |
| | | int valueLength = valuesBytes[vBytesPos] & 0x7F; |
| | | if (valueLength != valuesBytes[vBytesPos++]) |
| | | { |
| | | int valueLengthBytes = valueLength; |
| | | valueLength = 0; |
| | | for (int j=0; j < valueLengthBytes; j++, vBytesPos++) |
| | | { |
| | | valueLength = (valueLength << 8) | (valuesBytes[vBytesPos] & 0xFF); |
| | | } |
| | | } |
| | | |
| | | if(i == vOffset) |
| | | { |
| | | if(valueLength == 0) |
| | | { |
| | | return null; |
| | | } |
| | | else |
| | | { |
| | | byte[] valueBytes = new byte[valueLength]; |
| | | System.arraycopy(valuesBytes, vBytesPos, valueBytes, 0, valueLength); |
| | | return ByteString.wrap(valueBytes); |
| | | } |
| | | } |
| | | else |
| | | { |
| | | vBytesPos += valueLength; |
| | | } |
| | | } |
| | | return ByteString.empty(); |
| | | } |
| | | } |
| New file |
| | |
| | | /* |
| | | * CDDL HEADER START |
| | | * |
| | | * The contents of this file are subject to the terms of the |
| | | * Common Development and Distribution License, Version 1.0 only |
| | | * (the "License"). You may not use this file except in compliance |
| | | * with the License. |
| | | * |
| | | * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt |
| | | * or http://forgerock.org/license/CDDLv1.0.html. |
| | | * See the License for the specific language governing permissions |
| | | * and limitations under the License. |
| | | * |
| | | * When distributing Covered Code, include this CDDL HEADER in each |
| | | * file and include the License file at legal-notices/CDDLv1_0.txt. |
| | | * If applicable, add the following below this CDDL HEADER, with the |
| | | * fields enclosed by brackets "[]" replaced with your own identifying |
| | | * information: |
| | | * Portions Copyright [yyyy] [name of copyright owner] |
| | | * |
| | | * CDDL HEADER END |
| | | * |
| | | * |
| | | * Copyright 2006-2008 Sun Microsystems, Inc. |
| | | * Portions copyright 2011-2014 ForgeRock AS |
| | | */ |
| | | package org.opends.server.backends.pluggable; |
| | | |
| | | import org.forgerock.opendj.ldap.ByteString; |
| | | import org.opends.server.backends.pluggable.BackendImpl.ReadableStorage; |
| | | import org.opends.server.backends.pluggable.BackendImpl.Storage; |
| | | import org.opends.server.backends.pluggable.BackendImpl.StorageRuntimeException; |
| | | import org.opends.server.backends.pluggable.BackendImpl.TreeName; |
| | | import org.opends.server.backends.pluggable.BackendImpl.WriteableStorage; |
| | | import org.opends.server.util.StaticUtils; |
| | | |
| | | /** |
| | | * This class is responsible for storing the configuration state of |
| | | * the JE backend for a particular suffix. |
| | | */ |
| | | public class State extends DatabaseContainer |
| | | { |
| | | private static final ByteString falseBytes = ByteString.wrap(new byte[] { 0x00 }); |
| | | private static final ByteString trueBytes = ByteString.wrap(new byte[] { 0x01 }); |
| | | |
| | | /** |
| | | * Create a new State object. |
| | | * |
| | | * @param name The name of the entry database. |
| | | * @param env The JE Storage. |
| | | * @param entryContainer The entryContainer of the entry database. |
| | | */ |
| | | State(TreeName name, Storage env, EntryContainer entryContainer) |
| | | { |
| | | super(name, env, entryContainer); |
| | | } |
| | | |
| | | /** |
| | | * Return the key associated with the index in the state database. |
| | | * |
| | | * @param index The index we need the key for. |
| | | * @return the key |
| | | * @throws StorageRuntimeException If an error occurs in the JE database. |
| | | */ |
| | | private ByteString keyForIndex(DatabaseContainer index) |
| | | throws StorageRuntimeException |
| | | { |
| | | String shortName = index.getName().toString(); |
| | | return ByteString.wrap(StaticUtils.getBytes(shortName)); |
| | | } |
| | | |
| | | /** |
| | | * Remove a record from the entry database. |
| | | * |
| | | * @param txn The database transaction or null if none. |
| | | * @param index The index storing the trusted state info. |
| | | * @return true if the entry was removed, false if it was not. |
| | | * @throws StorageRuntimeException If an error occurs in the JE database. |
| | | */ |
| | | public boolean removeIndexTrustState(WriteableStorage txn, DatabaseContainer index) |
| | | throws StorageRuntimeException |
| | | { |
| | | ByteString key = keyForIndex(index); |
| | | return delete(txn, key); |
| | | } |
| | | |
| | | /** |
| | | * Fetch index state from the database. |
| | | * @param txn The database transaction or null if none. |
| | | * @param index The index storing the trusted state info. |
| | | * @return The trusted state of the index in the database. |
| | | * @throws StorageRuntimeException If an error occurs in the JE database. |
| | | */ |
| | | public boolean getIndexTrustState(ReadableStorage txn, DatabaseContainer index) |
| | | throws StorageRuntimeException |
| | | { |
| | | ByteString key = keyForIndex(index); |
| | | ByteString value = read(txn, key, false); |
| | | |
| | | if (value != null) |
| | | { |
| | | return value.equals(trueBytes); |
| | | } |
| | | return false; |
| | | } |
| | | |
| | | /** |
| | | * Put index state to database. |
| | | * @param txn The database transaction or null if none. |
| | | * @param index The index storing the trusted state info. |
| | | * @param trusted The state value to put into the database. |
| | | * @throws StorageRuntimeException If an error occurs in the JE database. |
| | | */ |
| | | public void putIndexTrustState(WriteableStorage txn, DatabaseContainer index, boolean trusted) |
| | | throws StorageRuntimeException |
| | | { |
| | | ByteString key = keyForIndex(index); |
| | | |
| | | txn.put(treeName, key, trusted ? trueBytes : falseBytes); |
| | | } |
| | | |
| | | } |
| New file |
| | |
| | | /* |
| | | * CDDL HEADER START |
| | | * |
| | | * The contents of this file are subject to the terms of the |
| | | * Common Development and Distribution License, Version 1.0 only |
| | | * (the "License"). You may not use this file except in compliance |
| | | * with the License. |
| | | * |
| | | * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt |
| | | * or http://forgerock.org/license/CDDLv1.0.html. |
| | | * See the License for the specific language governing permissions |
| | | * and limitations under the License. |
| | | * |
| | | * When distributing Covered Code, include this CDDL HEADER in each |
| | | * file and include the License file at legal-notices/CDDLv1_0.txt. |
| | | * If applicable, add the following below this CDDL HEADER, with the |
| | | * fields enclosed by brackets "[]" replaced with your own identifying |
| | | * information: |
| | | * Portions Copyright [yyyy] [name of copyright owner] |
| | | * |
| | | * CDDL HEADER END |
| | | * |
| | | * |
| | | * Copyright 2006-2008 Sun Microsystems, Inc. |
| | | * Portions Copyright 2011-2014 ForgeRock AS |
| | | */ |
| | | package org.opends.server.backends.pluggable; |
| | | |
| | | import java.util.ArrayList; |
| | | import java.util.Iterator; |
| | | import java.util.LinkedList; |
| | | import java.util.List; |
| | | import java.util.TreeSet; |
| | | import java.util.concurrent.atomic.AtomicInteger; |
| | | |
| | | import org.forgerock.i18n.LocalizableMessage; |
| | | import org.forgerock.i18n.slf4j.LocalizedLogger; |
| | | import org.forgerock.opendj.config.server.ConfigException; |
| | | import org.forgerock.opendj.ldap.ByteSequence; |
| | | import org.forgerock.opendj.ldap.ByteString; |
| | | import org.forgerock.opendj.ldap.ByteStringBuilder; |
| | | import org.forgerock.opendj.ldap.DecodeException; |
| | | import org.forgerock.opendj.ldap.ResultCode; |
| | | import org.forgerock.opendj.ldap.SearchScope; |
| | | import org.forgerock.opendj.ldap.SearchScope.Enum; |
| | | import org.forgerock.opendj.ldap.schema.MatchingRule; |
| | | import org.opends.server.admin.server.ConfigurationChangeListener; |
| | | import org.opends.server.admin.std.meta.LocalDBVLVIndexCfgDefn.Scope; |
| | | import org.opends.server.admin.std.server.LocalDBVLVIndexCfg; |
| | | import org.opends.server.backends.pluggable.BackendImpl.Cursor; |
| | | import org.opends.server.backends.pluggable.BackendImpl.ReadableStorage; |
| | | import org.opends.server.backends.pluggable.BackendImpl.Storage; |
| | | import org.opends.server.backends.pluggable.BackendImpl.StorageRuntimeException; |
| | | import org.opends.server.backends.pluggable.BackendImpl.WriteableStorage; |
| | | import org.opends.server.controls.ServerSideSortRequestControl; |
| | | import org.opends.server.controls.VLVRequestControl; |
| | | import org.opends.server.controls.VLVResponseControl; |
| | | import org.opends.server.core.DirectoryServer; |
| | | import org.opends.server.core.SearchOperation; |
| | | import org.opends.server.protocols.ldap.LDAPResultCode; |
| | | import org.opends.server.types.Attribute; |
| | | import org.opends.server.types.AttributeType; |
| | | import org.opends.server.types.ConfigChangeResult; |
| | | import org.opends.server.types.DN; |
| | | import org.opends.server.types.DirectoryException; |
| | | import org.opends.server.types.Entry; |
| | | import org.opends.server.types.Modification; |
| | | import org.opends.server.types.SearchFilter; |
| | | import org.opends.server.types.SortKey; |
| | | import org.opends.server.types.SortOrder; |
| | | import org.opends.server.util.StaticUtils; |
| | | |
| | | import com.sleepycat.je.LockMode; |
| | | |
| | | import static org.opends.messages.JebMessages.*; |
| | | import static org.opends.server.util.StaticUtils.*; |
| | | |
| | | /** |
| | | * This class represents a VLV index. Each database record is a sorted list |
| | | * of entry IDs followed by sets of attribute values used to sort the entries. |
| | | * The entire set of entry IDs are broken up into sorted subsets to decrease |
| | | * the number of database retrievals needed for a range lookup. The records are |
| | | * keyed by the last entry's first sort attribute value. The list of entries |
| | | * in a particular database record maintains the property where the first sort |
| | | * attribute value is bigger then the previous key but smaller or equal |
| | | * to its own key. |
| | | */ |
| | | public class VLVIndex extends DatabaseContainer |
| | | implements ConfigurationChangeListener<LocalDBVLVIndexCfg> |
| | | { |
| | | private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); |
| | | |
| | | /** The comparator for vlvIndex keys. */ |
| | | public VLVKeyComparator comparator; |
| | | /** The limit on the number of entry IDs that may be indexed by one key. */ |
| | | private int sortedSetCapacity = 4000; |
| | | /** The SortOrder in use by this VLV index to sort the entries. */ |
| | | public SortOrder sortOrder; |
| | | |
| | | /** The cached count of entries in this index. */ |
| | | private final AtomicInteger count; |
| | | |
| | | private final State state; |
| | | /** |
| | | * A flag to indicate if this vlvIndex should be trusted to be consistent |
| | | * with the entries database. |
| | | */ |
| | | private boolean trusted; |
| | | /** A flag to indicate if a rebuild process is running on this vlvIndex. */ |
| | | private boolean rebuildRunning; |
| | | |
| | | /** The VLV vlvIndex configuration. */ |
| | | private LocalDBVLVIndexCfg config; |
| | | |
| | | private DN baseDN; |
| | | private SearchFilter filter; |
| | | private SearchScope scope; |
| | | |
| | | |
| | | /** |
| | | * Create a new VLV vlvIndex object. |
| | | * |
| | | * @param config The VLV index config object to use for this VLV |
| | | * index. |
| | | * @param state The state database to persist vlvIndex state info. |
| | | * @param env The JE Storage |
| | | * @param entryContainer The database entryContainer holding this vlvIndex. |
| | | * @throws StorageRuntimeException |
| | | * If an error occurs in the JE database. |
| | | * @throws ConfigException if a error occurs while reading the VLV index |
| | | * configuration |
| | | */ |
| | | public VLVIndex(LocalDBVLVIndexCfg config, State state, Storage env, |
| | | EntryContainer entryContainer) |
| | | throws StorageRuntimeException, ConfigException |
| | | { |
| | | super(entryContainer.getDatabasePrefix().child("vlv."+config.getName()), |
| | | env, entryContainer); |
| | | |
| | | this.config = config; |
| | | this.baseDN = config.getBaseDN(); |
| | | this.scope = valueOf(config.getScope()); |
| | | this.sortedSetCapacity = config.getMaxBlockSize(); |
| | | |
| | | try |
| | | { |
| | | this.filter = SearchFilter.createFilterFromString(config.getFilter()); |
| | | } |
| | | catch(Exception e) |
| | | { |
| | | LocalizableMessage msg = ERR_JEB_CONFIG_VLV_INDEX_BAD_FILTER.get( |
| | | config.getFilter(), treeName, stackTraceToSingleLineString(e)); |
| | | throw new ConfigException(msg); |
| | | } |
| | | |
| | | String[] sortAttrs = config.getSortOrder().split(" "); |
| | | SortKey[] sortKeys = new SortKey[sortAttrs.length]; |
| | | MatchingRule[] orderingRules = new MatchingRule[sortAttrs.length]; |
| | | boolean[] ascending = new boolean[sortAttrs.length]; |
| | | for(int i = 0; i < sortAttrs.length; i++) |
| | | { |
| | | try |
| | | { |
| | | if(sortAttrs[i].startsWith("-")) |
| | | { |
| | | ascending[i] = false; |
| | | sortAttrs[i] = sortAttrs[i].substring(1); |
| | | } |
| | | else |
| | | { |
| | | ascending[i] = true; |
| | | if(sortAttrs[i].startsWith("+")) |
| | | { |
| | | sortAttrs[i] = sortAttrs[i].substring(1); |
| | | } |
| | | } |
| | | } |
| | | catch(Exception e) |
| | | { |
| | | throw new ConfigException(ERR_JEB_CONFIG_VLV_INDEX_UNDEFINED_ATTR.get(sortKeys[i], treeName)); |
| | | } |
| | | |
| | | AttributeType attrType = |
| | | DirectoryServer.getAttributeType(sortAttrs[i].toLowerCase()); |
| | | if(attrType == null) |
| | | { |
| | | LocalizableMessage msg = |
| | | ERR_JEB_CONFIG_VLV_INDEX_UNDEFINED_ATTR.get(sortAttrs[i], treeName); |
| | | throw new ConfigException(msg); |
| | | } |
| | | sortKeys[i] = new SortKey(attrType, ascending[i]); |
| | | orderingRules[i] = attrType.getOrderingMatchingRule(); |
| | | } |
| | | |
| | | this.sortOrder = new SortOrder(sortKeys); |
| | | this.comparator = new VLVKeyComparator(orderingRules, ascending); |
| | | |
| | | this.state = state; |
| | | this.trusted = state.getIndexTrustState(null, this); |
| | | if (!trusted && entryContainer.getHighestEntryID().longValue() == 0) |
| | | { |
| | | // If there are no entries in the entry container then there |
| | | // is no reason why this vlvIndex can't be upgraded to trusted. |
| | | setTrusted(null, true); |
| | | } |
| | | |
| | | this.count = new AtomicInteger(0); |
| | | this.config.addChangeListener(this); |
| | | } |
| | | |
| | | private SearchScope valueOf(Scope cfgScope) |
| | | { |
| | | final Enum toFind = SearchScope.Enum.valueOf(cfgScope.name()); |
| | | for (SearchScope scope : SearchScope.values()) |
| | | { |
| | | if (scope.asEnum() == toFind) |
| | | { |
| | | return scope; |
| | | } |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public void open() throws StorageRuntimeException |
| | | { |
| | | super.open(); |
| | | |
| | | Cursor cursor = storage.openCursor(treeName); |
| | | try |
| | | { |
| | | while (cursor.next()) |
| | | { |
| | | count.getAndAdd(SortValuesSet.getEncodedSize(cursor.getValue(), 0)); |
| | | } |
| | | } |
| | | finally |
| | | { |
| | | cursor.close(); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Close the VLV index. |
| | | * |
| | | * @throws StorageRuntimeException if a JE database error occurs while |
| | | * closing the index. |
| | | */ |
| | | @Override |
| | | public void close() throws StorageRuntimeException |
| | | { |
| | | super.close(); |
| | | this.config.removeChangeListener(this); |
| | | } |
| | | |
| | | /** |
| | | * Update the vlvIndex for a new entry. |
| | | * |
| | | * @param txn A database transaction, or null if none is required. |
| | | * @param entryID The entry ID. |
| | | * @param entry The entry to be indexed. |
| | | * @return True if the entry ID for the entry are added. False if |
| | | * the entry ID already exists. |
| | | * @throws StorageRuntimeException If an error occurs in the JE database. |
| | | * @throws org.opends.server.types.DirectoryException If a Directory Server |
| | | * error occurs. |
| | | * @throws JebException If an error occurs in the JE backend. |
| | | */ |
| | | public boolean addEntry(WriteableStorage txn, EntryID entryID, Entry entry) |
| | | throws StorageRuntimeException, DirectoryException, JebException |
| | | { |
| | | return shouldInclude(entry) |
| | | && insertValues(txn, entryID.longValue(), entry); |
| | | } |
| | | |
| | | /** |
| | | * Update the vlvIndex for a new entry. |
| | | * |
| | | * @param buffer The index buffer to buffer the changes. |
| | | * @param entryID The entry ID. |
| | | * @param entry The entry to be indexed. |
| | | * @return True if the entry ID for the entry are added. False if |
| | | * the entry ID already exists. |
| | | * @throws DirectoryException If a Directory Server |
| | | * error occurs. |
| | | */ |
| | | public boolean addEntry(IndexBuffer buffer, EntryID entryID, Entry entry) |
| | | throws DirectoryException |
| | | { |
| | | if (shouldInclude(entry)) |
| | | { |
| | | final SortValues sortValues = new SortValues(entryID, entry, sortOrder); |
| | | buffer.getVLVIndex(this).addValues(sortValues); |
| | | return true; |
| | | } |
| | | return false; |
| | | } |
| | | |
| | | /** |
| | | * Update the vlvIndex for a deleted entry. |
| | | * |
| | | * @param buffer The database transaction to be used for the deletions |
| | | * @param entryID The entry ID |
| | | * @param entry The contents of the deleted entry. |
| | | * @return True if the entry was successfully removed from this VLV index |
| | | * or False otherwise. |
| | | * @throws DirectoryException If a Directory Server error occurs. |
| | | */ |
| | | public boolean removeEntry(IndexBuffer buffer, EntryID entryID, Entry entry) |
| | | throws DirectoryException |
| | | { |
| | | if (shouldInclude(entry)) |
| | | { |
| | | final SortValues sortValues = new SortValues(entryID, entry, sortOrder); |
| | | buffer.getVLVIndex(this).deleteValues(sortValues); |
| | | return true; |
| | | } |
| | | return false; |
| | | } |
| | | |
| | | /** |
| | | * Update the vlvIndex to reflect a sequence of modifications in a Modify |
| | | * operation. |
| | | * |
| | | * @param buffer The database transaction to be used for the deletions |
| | | * @param entryID The ID of the entry that was modified. |
| | | * @param oldEntry The entry before the modifications were applied. |
| | | * @param newEntry The entry after the modifications were applied. |
| | | * @param mods The sequence of modifications in the Modify operation. |
| | | * @return True if the modification was successfully processed or False |
| | | * otherwise. |
| | | * @throws StorageRuntimeException If an error occurs during an operation on a |
| | | * JE database. |
| | | * @throws DirectoryException If a Directory Server error occurs. |
| | | */ |
| | | public boolean modifyEntry(IndexBuffer buffer, |
| | | EntryID entryID, |
| | | Entry oldEntry, |
| | | Entry newEntry, |
| | | List<Modification> mods) |
| | | throws StorageRuntimeException, DirectoryException |
| | | { |
| | | if (shouldInclude(oldEntry)) |
| | | { |
| | | if (shouldInclude(newEntry)) |
| | | { |
| | | // The entry should still be indexed. See if any sorted attributes are |
| | | // changed. |
| | | if (isSortAttributeModified(mods)) |
| | | { |
| | | boolean success; |
| | | // Sorted attributes have changed. Reindex the entry; |
| | | success = removeEntry(buffer, entryID, oldEntry); |
| | | success &= addEntry(buffer, entryID, newEntry); |
| | | return success; |
| | | } |
| | | } |
| | | else |
| | | { |
| | | // The modifications caused the new entry to be unindexed. Remove from |
| | | // vlvIndex. |
| | | return removeEntry(buffer, entryID, oldEntry); |
| | | } |
| | | } |
| | | else |
| | | { |
| | | if (shouldInclude(newEntry)) |
| | | { |
| | | // The modifications caused the new entry to be indexed. Add to vlvIndex |
| | | return addEntry(buffer, entryID, newEntry); |
| | | } |
| | | } |
| | | |
| | | // The modifications does not affect this vlvIndex |
| | | return true; |
| | | } |
| | | |
| | | private boolean isSortAttributeModified(List<Modification> mods) |
| | | { |
| | | for (SortKey sortKey : sortOrder.getSortKeys()) |
| | | { |
| | | AttributeType attributeType = sortKey.getAttributeType(); |
| | | Iterable<AttributeType> subTypes = DirectoryServer.getSchema().getSubTypes(attributeType); |
| | | for (Modification mod : mods) |
| | | { |
| | | AttributeType modAttrType = mod.getAttribute().getAttributeType(); |
| | | if (modAttrType.equals(attributeType)) |
| | | { |
| | | return true; |
| | | } |
| | | for (AttributeType subType : subTypes) |
| | | { |
| | | if (modAttrType.equals(subType)) |
| | | { |
| | | return true; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | return false; |
| | | } |
| | | |
| | | /** |
| | | * Get a sorted values set that should contain the entry with the given |
| | | * information. |
| | | * |
| | | * @param txn The transaction to use when retrieving the set or NULL if it is |
| | | * not required. |
| | | * @param entryID The entry ID to use. |
| | | * @param values The values to use. |
| | | * @param types The types of the values to use. |
| | | * @return The SortValuesSet that should contain the entry with the given |
| | | * information. |
| | | * @throws StorageRuntimeException If an error occurs during an operation on a |
| | | * JE database. |
| | | * @throws DirectoryException If a Directory Server error occurs. |
| | | */ |
| | | public SortValuesSet getSortValuesSet(ReadableStorage txn, long entryID, |
| | | ByteString[] values, AttributeType[] types) throws StorageRuntimeException, |
| | | DirectoryException |
| | | { |
| | | ByteString key = encodeKey(entryID, values, types); |
| | | return getSortValuesSet(txn, key, false); |
| | | } |
| | | |
| | | private SortValuesSet getSortValuesSet(ReadableStorage txn, ByteString key, boolean isRMW) |
| | | { |
| | | ByteString value = isRMW ? txn.getRMW(treeName, key) : txn.get(treeName, key); |
| | | if (value == null) |
| | | { |
| | | // There are no records in the database |
| | | if (logger.isTraceEnabled()) |
| | | { |
| | | logger.trace("No sort values set exist in VLV vlvIndex %s. " |
| | | + "Creating unbound set.", config.getName()); |
| | | } |
| | | // this could not be found, so clean the key for later reuse |
| | | return new SortValuesSet(this); |
| | | } |
| | | |
| | | if (logger.isTraceEnabled()) |
| | | { |
| | | logSearchKeyResult(key); |
| | | } |
| | | return new SortValuesSet(key, value, this); |
| | | } |
| | | |
| | | private void logSearchKeyResult(ByteString key) |
| | | { |
| | | StringBuilder searchKeyHex = new StringBuilder(); |
| | | StaticUtils.byteArrayToHexPlusAscii(searchKeyHex, key.toByteArray(), 4); |
| | | StringBuilder foundKeyHex = new StringBuilder(); |
| | | StaticUtils.byteArrayToHexPlusAscii(foundKeyHex, key.toByteArray(), 4); |
| | | logger.trace("Retrieved a sort values set in VLV vlvIndex %s\n" + |
| | | "Search Key:%s\nFound Key:%s\n", |
| | | config.getName(), searchKeyHex, foundKeyHex); |
| | | } |
| | | |
| | | /** |
| | | * Search for entries matching the entry ID and attribute values and |
| | | * return its entry ID. |
| | | * |
| | | * @param txn The JE transaction to use for database updates. |
| | | * @param entryID The entry ID to search for. |
| | | * @param values The values to search for. |
| | | * @param types The types of the values to search for. |
| | | * @return The index of the entry ID matching the values or -1 if its not |
| | | * found. |
| | | * @throws StorageRuntimeException If an error occurs during an operation on a |
| | | * JE database. |
| | | * @throws JebException If an error occurs during an operation on a |
| | | * JE database. |
| | | * @throws DirectoryException If a Directory Server error occurs. |
| | | */ |
| | | public boolean containsValues(ReadableStorage txn, long entryID, |
| | | ByteString[] values, AttributeType[] types) throws JebException, |
| | | StorageRuntimeException, DirectoryException |
| | | { |
| | | SortValuesSet valuesSet = getSortValuesSet(txn, entryID, values, types); |
| | | int pos = valuesSet.binarySearch(entryID, values); |
| | | return pos >= 0; |
| | | } |
| | | |
| | | private boolean insertValues(WriteableStorage txn, long entryID, Entry entry) |
| | | throws JebException, StorageRuntimeException, DirectoryException |
| | | { |
| | | ByteString[] values = getSortValues(entry); |
| | | AttributeType[] types = getSortTypes(); |
| | | ByteString key = encodeKey(entryID, values, types); |
| | | |
| | | SortValuesSet sortValuesSet = getSortValuesSet(txn, key, true); |
| | | boolean success = sortValuesSet.add(entryID, values, types); |
| | | |
| | | int newSize = sortValuesSet.size(); |
| | | if(newSize >= sortedSetCapacity) |
| | | { |
| | | SortValuesSet splitSortValuesSet = sortValuesSet.split(newSize / 2); |
| | | put(txn, splitSortValuesSet); // splitAfter |
| | | put(txn, sortValuesSet); // after |
| | | |
| | | if(logger.isTraceEnabled()) |
| | | { |
| | | logger.trace("SortValuesSet with key %s has reached" + |
| | | " the entry size of %d. Spliting into two sets with " + |
| | | " keys %s and %s.", splitSortValuesSet.getKeySortValues(), |
| | | newSize, sortValuesSet.getKeySortValues(), |
| | | splitSortValuesSet.getKeySortValues()); |
| | | } |
| | | } |
| | | else |
| | | { |
| | | ByteString after = sortValuesSet.toByteString(); |
| | | put(txn, key, after); |
| | | // TODO: What about phantoms? |
| | | } |
| | | |
| | | if(success) |
| | | { |
| | | count.getAndIncrement(); |
| | | } |
| | | |
| | | return success; |
| | | } |
| | | |
| | | private void put(WriteableStorage txn, SortValuesSet set) throws DirectoryException |
| | | { |
| | | put(txn, set.getKeyBytes(), set.toByteString()); |
| | | } |
| | | |
| | | /** |
| | | * Gets the types of the attribute values to sort. |
| | | * |
| | | * @return The types of the attribute values to sort on. |
| | | */ |
| | | AttributeType[] getSortTypes() |
| | | { |
| | | SortKey[] sortKeys = sortOrder.getSortKeys(); |
| | | AttributeType[] types = new AttributeType[sortKeys.length]; |
| | | for (int i = 0; i < sortKeys.length; i++) |
| | | { |
| | | types[i] = sortKeys[i].getAttributeType(); |
| | | } |
| | | return types; |
| | | } |
| | | |
| | | private boolean getSearchKeyRange(ReadableStorage txn, ByteString key) |
| | | { |
| | | Cursor cursor = txn.openCursor(treeName); |
| | | try |
| | | { |
| | | return cursor.positionToKeyOrNext(key); |
| | | } |
| | | finally |
| | | { |
| | | cursor.close(); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Update the vlvIndex with the specified values to add and delete. |
| | | * |
| | | * @param txn A database transaction, or null if none is required. |
| | | * @param addedValues The values to add to the VLV index. |
| | | * @param deletedValues The values to delete from the VLV index. |
| | | * @throws StorageRuntimeException If an error occurs in the JE database. |
| | | * @throws DirectoryException If a Directory Server |
| | | * error occurs. |
| | | */ |
| | | public void updateIndex(WriteableStorage txn, |
| | | TreeSet<SortValues> addedValues, |
| | | TreeSet<SortValues> deletedValues) |
| | | throws DirectoryException, StorageRuntimeException |
| | | { |
| | | // Handle cases where nothing is changed early to avoid |
| | | // DB access. |
| | | if((addedValues == null || addedValues.isEmpty()) && |
| | | (deletedValues == null || deletedValues.isEmpty())) |
| | | { |
| | | return; |
| | | } |
| | | |
| | | Iterator<SortValues> aValues = null; |
| | | Iterator<SortValues> dValues = null; |
| | | SortValues av = null; |
| | | SortValues dv = null; |
| | | |
| | | if(addedValues != null) |
| | | { |
| | | aValues = addedValues.iterator(); |
| | | av = aValues.next(); |
| | | } |
| | | if(deletedValues != null) |
| | | { |
| | | dValues = deletedValues.iterator(); |
| | | dv = dValues.next(); |
| | | } |
| | | |
| | | while(true) |
| | | { |
| | | ByteString key; |
| | | if(av != null) |
| | | { |
| | | if(dv != null) |
| | | { |
| | | // Start from the smallest values from either set. |
| | | if(av.compareTo(dv) < 0) |
| | | { |
| | | key = encodeKey(av); |
| | | } |
| | | else |
| | | { |
| | | key = encodeKey(dv); |
| | | } |
| | | } |
| | | else |
| | | { |
| | | key = encodeKey(av); |
| | | } |
| | | } |
| | | else if(dv != null) |
| | | { |
| | | key = encodeKey(dv); |
| | | } |
| | | else |
| | | { |
| | | break; |
| | | } |
| | | |
| | | final SortValuesSet sortValuesSet = getSortValuesSet(txn, key, true); |
| | | int oldSize = sortValuesSet.size(); |
| | | if(key.length() == 0) |
| | | { |
| | | // This is the last unbounded set. |
| | | while(av != null) |
| | | { |
| | | sortValuesSet.add(av.getEntryID(), av.getValues(), av.getTypes()); |
| | | av = moveToNextSortValues(aValues); |
| | | } |
| | | |
| | | while(dv != null) |
| | | { |
| | | sortValuesSet.remove(dv.getEntryID(), dv.getValues()); |
| | | dv = moveToNextSortValues(dValues); |
| | | } |
| | | } |
| | | else |
| | | { |
| | | SortValues maxValues = decodeKey(sortValuesSet.getKeyBytes()); |
| | | |
| | | while(av != null && av.compareTo(maxValues) <= 0) |
| | | { |
| | | sortValuesSet.add(av.getEntryID(), av.getValues(), av.getTypes()); |
| | | av = moveToNextSortValues(aValues); |
| | | } |
| | | |
| | | while(dv != null && dv.compareTo(maxValues) <= 0) |
| | | { |
| | | sortValuesSet.remove(dv.getEntryID(), dv.getValues()); |
| | | dv = moveToNextSortValues(dValues); |
| | | } |
| | | } |
| | | |
| | | int newSize = sortValuesSet.size(); |
| | | if(newSize >= sortedSetCapacity) |
| | | { |
| | | SortValuesSet splitSortValuesSet = sortValuesSet.split(newSize / 2); |
| | | put(txn, splitSortValuesSet); // splitAfter |
| | | put(txn, sortValuesSet); // after |
| | | |
| | | if(logger.isTraceEnabled()) |
| | | { |
| | | logger.trace("SortValuesSet with key %s has reached" + |
| | | " the entry size of %d. Spliting into two sets with " + |
| | | " keys %s and %s.", splitSortValuesSet.getKeySortValues(), |
| | | newSize, sortValuesSet.getKeySortValues(), |
| | | splitSortValuesSet.getKeySortValues()); |
| | | } |
| | | } |
| | | else if(newSize == 0) |
| | | { |
| | | delete(txn, key); |
| | | } |
| | | else |
| | | { |
| | | ByteString after = sortValuesSet.toByteString(); |
| | | put(txn, key, after); |
| | | } |
| | | |
| | | count.getAndAdd(newSize - oldSize); |
| | | } |
| | | } |
| | | |
| | | private SortValues moveToNextSortValues(Iterator<SortValues> sortValues) |
| | | { |
| | | sortValues.remove(); |
| | | if (sortValues.hasNext()) |
| | | { |
| | | return sortValues.next(); |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | private ByteString encodeKey(SortValues sv) throws DirectoryException |
| | | { |
| | | return encodeKey(sv.getEntryID(), sv.getValues(), sv.getTypes()); |
| | | } |
| | | |
| | | /** |
| | | * Evaluate a search with sort control using this VLV index. |
| | | * |
| | | * @param txn The transaction to used when reading the index or NULL if it is |
| | | * not required. |
| | | * @param searchOperation The search operation to evaluate. |
| | | * @param sortControl The sort request control to evaluate. |
| | | * @param vlvRequest The VLV request control to evaluate or NULL if VLV is not |
| | | * requested. |
| | | * @param debugBuilder If not null, a diagnostic string will be written |
| | | * which will help determine how this index contributed |
| | | * to this search. |
| | | * @return The sorted EntryIDSet containing the entry IDs that match the |
| | | * search criteria. |
| | | * @throws DirectoryException If a Directory Server error occurs. |
| | | * @throws StorageRuntimeException If an error occurs in the JE database. |
| | | */ |
| | | public EntryIDSet evaluate(ReadableStorage txn, |
| | | SearchOperation searchOperation, |
| | | ServerSideSortRequestControl sortControl, |
| | | VLVRequestControl vlvRequest, |
| | | StringBuilder debugBuilder) |
| | | throws DirectoryException, StorageRuntimeException |
| | | { |
| | | if (!trusted || rebuildRunning |
| | | || !searchOperation.getBaseDN().equals(baseDN) |
| | | || !searchOperation.getScope().equals(scope) |
| | | || !searchOperation.getFilter().equals(filter) |
| | | || !sortControl.getSortOrder().equals(sortOrder)) |
| | | { |
| | | return null; |
| | | } |
| | | |
| | | if (debugBuilder != null) |
| | | { |
| | | debugBuilder.append("vlv="); |
| | | debugBuilder.append("[INDEX:"); |
| | | debugBuilder.append(treeName.replace(entryContainer.getDatabasePrefix() + "_", "")); |
| | | debugBuilder.append("]"); |
| | | } |
| | | |
| | | long[] selectedIDs = new long[0]; |
| | | if(vlvRequest != null) |
| | | { |
| | | int currentCount = count.get(); |
| | | int beforeCount = vlvRequest.getBeforeCount(); |
| | | int afterCount = vlvRequest.getAfterCount(); |
| | | |
| | | if (vlvRequest.getTargetType() == VLVRequestControl.TYPE_TARGET_BYOFFSET) |
| | | { |
| | | int targetOffset = vlvRequest.getOffset(); |
| | | if (targetOffset < 0) |
| | | { |
| | | // The client specified a negative target offset. This should never |
| | | // be allowed. |
| | | searchOperation.addResponseControl( |
| | | new VLVResponseControl(targetOffset, currentCount, |
| | | LDAPResultCode.OFFSET_RANGE_ERROR)); |
| | | |
| | | LocalizableMessage message = ERR_ENTRYIDSORTER_NEGATIVE_START_POS.get(); |
| | | throw new DirectoryException(ResultCode.VIRTUAL_LIST_VIEW_ERROR, |
| | | message); |
| | | } |
| | | else if (targetOffset == 0) |
| | | { |
| | | // This is an easy mistake to make, since VLV offsets start at 1 |
| | | // instead of 0. We'll assume the client meant to use 1. |
| | | targetOffset = 1; |
| | | } |
| | | int listOffset = targetOffset - 1; // VLV offsets start at 1, not 0. |
| | | int startPos = listOffset - beforeCount; |
| | | if (startPos < 0) |
| | | { |
| | | // This can happen if beforeCount >= offset, and in this case we'll |
| | | // just adjust the start position to ignore the range of beforeCount |
| | | // that doesn't exist. |
| | | startPos = 0; |
| | | beforeCount = listOffset; |
| | | } |
| | | else if(startPos >= currentCount) |
| | | { |
| | | // The start position is beyond the end of the list. In this case, |
| | | // we'll assume that the start position was one greater than the |
| | | // size of the list and will only return the beforeCount entries. |
| | | // The start position is beyond the end of the list. In this case, |
| | | // we'll assume that the start position was one greater than the |
| | | // size of the list and will only return the beforeCount entries. |
| | | targetOffset = currentCount + 1; |
| | | listOffset = currentCount; |
| | | startPos = listOffset - beforeCount; |
| | | afterCount = 0; |
| | | } |
| | | |
| | | int count = 1 + beforeCount + afterCount; |
| | | selectedIDs = new long[count]; |
| | | |
| | | Cursor cursor = txn.openCursor(treeName); |
| | | try |
| | | { |
| | | //Locate the set that contains the target entry. |
| | | int cursorCount = 0; |
| | | int selectedPos = 0; |
| | | while (cursor.next()) |
| | | { |
| | | if(logger.isTraceEnabled()) |
| | | { |
| | | logSearchKeyResult(cursor.getKey()); |
| | | } |
| | | long[] IDs = SortValuesSet.getEncodedIDs(cursor.getValue(), 0); |
| | | for(int i = startPos + selectedPos - cursorCount; |
| | | i < IDs.length && selectedPos < count; |
| | | i++, selectedPos++) |
| | | { |
| | | selectedIDs[selectedPos] = IDs[i]; |
| | | } |
| | | cursorCount += IDs.length; |
| | | } |
| | | |
| | | if (selectedPos < count) |
| | | { |
| | | // We don't have enough entries in the set to meet the requested |
| | | // page size, so we'll need to shorten the array. |
| | | long[] newIDArray = new long[selectedPos]; |
| | | System.arraycopy(selectedIDs, 0, newIDArray, 0, selectedPos); |
| | | selectedIDs = newIDArray; |
| | | } |
| | | |
| | | searchOperation.addResponseControl( |
| | | new VLVResponseControl(targetOffset, currentCount, |
| | | LDAPResultCode.SUCCESS)); |
| | | |
| | | if(debugBuilder != null) |
| | | { |
| | | debugBuilder.append("[COUNT:"); |
| | | debugBuilder.append(cursorCount); |
| | | debugBuilder.append("]"); |
| | | } |
| | | } |
| | | finally |
| | | { |
| | | cursor.close(); |
| | | } |
| | | } |
| | | else |
| | | { |
| | | int targetOffset = 0; |
| | | int includedBeforeCount = 0; |
| | | int includedAfterCount = 0; |
| | | LinkedList<EntryID> idList = new LinkedList<EntryID>(); |
| | | |
| | | Cursor cursor = openCursor(txn); |
| | | try |
| | | { |
| | | LockMode lockMode = LockMode.DEFAULT; |
| | | ByteSequence vBytes = vlvRequest.getGreaterThanOrEqualAssertion(); |
| | | ByteStringBuilder keyBytes = new ByteStringBuilder(vBytes.length() + 4); |
| | | keyBytes.appendBERLength(vBytes.length()); |
| | | vBytes.copyTo(keyBytes); |
| | | |
| | | boolean success = cursor.positionToKeyOrNext(keyBytes); |
| | | if (success) |
| | | { |
| | | if(logger.isTraceEnabled()) |
| | | { |
| | | logSearchKeyResult(cursor.getKey()); |
| | | } |
| | | SortValuesSet sortValuesSet = |
| | | new SortValuesSet(cursor.getKey(), cursor.getValue(), this); |
| | | |
| | | int adjustedTargetOffset = sortValuesSet.binarySearch( |
| | | -1, vlvRequest.getGreaterThanOrEqualAssertion()); |
| | | if(adjustedTargetOffset < 0) |
| | | { |
| | | // For a negative return value r, the vlvIndex -(r+1) gives the |
| | | // array index of the ID that is greater then the assertion value. |
| | | adjustedTargetOffset = -(adjustedTargetOffset+1); |
| | | } |
| | | |
| | | targetOffset = adjustedTargetOffset; |
| | | |
| | | // Iterate through all the sort values sets before this one to find |
| | | // the target offset in the index. |
| | | int lastOffset = adjustedTargetOffset - 1; |
| | | long[] lastIDs = sortValuesSet.getEntryIDs(); |
| | | while(true) |
| | | { |
| | | for(int i = lastOffset; |
| | | i >= 0 && includedBeforeCount < beforeCount; i--) |
| | | { |
| | | idList.addFirst(new EntryID(lastIDs[i])); |
| | | includedBeforeCount++; |
| | | } |
| | | |
| | | success = cursor.previous(); |
| | | if (success) |
| | | { |
| | | break; |
| | | } |
| | | |
| | | if(includedBeforeCount < beforeCount) |
| | | { |
| | | lastIDs = SortValuesSet.getEncodedIDs(cursor.getValue(), 0); |
| | | lastOffset = lastIDs.length - 1; |
| | | targetOffset += lastIDs.length; |
| | | } |
| | | else |
| | | { |
| | | targetOffset += SortValuesSet.getEncodedSize(cursor.getValue(), 0); |
| | | } |
| | | } |
| | | |
| | | |
| | | // Set the cursor back to the position of the target entry set |
| | | cursor.positionToKey(sortValuesSet.getKeyBytes()); |
| | | |
| | | // Add the target and after count entries if the target was found. |
| | | lastOffset = adjustedTargetOffset; |
| | | lastIDs = sortValuesSet.getEntryIDs(); |
| | | int afterIDCount = 0; |
| | | while(true) |
| | | { |
| | | for(int i = lastOffset; |
| | | i < lastIDs.length && includedAfterCount < afterCount + 1; |
| | | i++) |
| | | { |
| | | idList.addLast(new EntryID(lastIDs[i])); |
| | | includedAfterCount++; |
| | | } |
| | | |
| | | if(includedAfterCount >= afterCount + 1) |
| | | { |
| | | break; |
| | | } |
| | | |
| | | success = cursor.next(); |
| | | if (success) |
| | | { |
| | | break; |
| | | } |
| | | |
| | | lastIDs = SortValuesSet.getEncodedIDs(cursor.getValue(), 0); |
| | | lastOffset = 0; |
| | | afterIDCount += lastIDs.length; |
| | | } |
| | | |
| | | selectedIDs = new long[idList.size()]; |
| | | Iterator<EntryID> idIterator = idList.iterator(); |
| | | for (int i=0; i < selectedIDs.length; i++) |
| | | { |
| | | selectedIDs[i] = idIterator.next().longValue(); |
| | | } |
| | | |
| | | searchOperation.addResponseControl( |
| | | new VLVResponseControl(targetOffset + 1, currentCount, |
| | | LDAPResultCode.SUCCESS)); |
| | | |
| | | if(debugBuilder != null) |
| | | { |
| | | debugBuilder.append("[COUNT:"); |
| | | debugBuilder.append(targetOffset + afterIDCount + 1); |
| | | debugBuilder.append("]"); |
| | | } |
| | | } |
| | | } |
| | | finally |
| | | { |
| | | cursor.close(); |
| | | } |
| | | } |
| | | } |
| | | else |
| | | { |
| | | LinkedList<long[]> idSets = new LinkedList<long[]>(); |
| | | int currentCount = 0; |
| | | |
| | | Cursor cursor = openCursor(txn); |
| | | try |
| | | { |
| | | while (cursor.next()) |
| | | { |
| | | if(logger.isTraceEnabled()) |
| | | { |
| | | logSearchKeyResult(cursor.getKey()); |
| | | } |
| | | long[] ids = SortValuesSet.getEncodedIDs(cursor.getValue(), 0); |
| | | idSets.add(ids); |
| | | currentCount += ids.length; |
| | | } |
| | | } |
| | | finally |
| | | { |
| | | cursor.close(); |
| | | } |
| | | |
| | | selectedIDs = new long[currentCount]; |
| | | int pos = 0; |
| | | for(long[] id : idSets) |
| | | { |
| | | System.arraycopy(id, 0, selectedIDs, pos, id.length); |
| | | pos += id.length; |
| | | } |
| | | |
| | | if(debugBuilder != null) |
| | | { |
| | | debugBuilder.append("[COUNT:"); |
| | | debugBuilder.append(currentCount); |
| | | debugBuilder.append("]"); |
| | | } |
| | | } |
| | | return new EntryIDSet(selectedIDs, 0, selectedIDs.length); |
| | | } |
| | | |
| | | /** |
| | | * Set the vlvIndex trust state. |
| | | * @param txn A database transaction, or null if none is required. |
| | | * @param trusted True if this vlvIndex should be trusted or false |
| | | * otherwise. |
| | | * @throws StorageRuntimeException If an error occurs in the JE database. |
| | | */ |
| | | public synchronized void setTrusted(WriteableStorage txn, boolean trusted) |
| | | throws StorageRuntimeException |
| | | { |
| | | this.trusted = trusted; |
| | | state.putIndexTrustState(txn, this, trusted); |
| | | } |
| | | |
| | | /** |
| | | * Return true iff this index is trusted. |
| | | * @return the trusted state of this index |
| | | */ |
| | | public boolean isTrusted() |
| | | { |
| | | return trusted; |
| | | } |
| | | |
| | | /** |
| | | * Set the rebuild status of this vlvIndex. |
| | | * @param rebuildRunning True if a rebuild process on this vlvIndex |
| | | * is running or False otherwise. |
| | | */ |
| | | public synchronized void setRebuildStatus(boolean rebuildRunning) |
| | | { |
| | | this.rebuildRunning = rebuildRunning; |
| | | } |
| | | |
| | | /** |
| | | * Gets the values to sort on from the entry. |
| | | * |
| | | * @param entry The entry to get the values from. |
| | | * @return The attribute values to sort on. |
| | | */ |
| | | ByteString[] getSortValues(Entry entry) |
| | | { |
| | | SortKey[] sortKeys = sortOrder.getSortKeys(); |
| | | ByteString[] values = new ByteString[sortKeys.length]; |
| | | for (int i=0; i < sortKeys.length; i++) |
| | | { |
| | | SortKey sortKey = sortKeys[i]; |
| | | List<Attribute> attrList = entry.getAttribute(sortKey.getAttributeType()); |
| | | if (attrList != null) |
| | | { |
| | | // There may be multiple versions of this attribute in the target entry |
| | | // (e.g., with different sets of options), and it may also be a |
| | | // multivalued attribute. In that case, we need to find the value that |
| | | // is the best match for the corresponding sort key (i.e., for sorting |
| | | // in ascending order, we want to find the lowest value; for sorting in |
| | | // descending order, we want to find the highest value). This is |
| | | // handled by the SortKey.compareValues method. |
| | | ByteString sortValue = null; |
| | | for (Attribute a : attrList) |
| | | { |
| | | for (ByteString v : a) |
| | | { |
| | | if (sortValue == null || sortKey.compareValues(v, sortValue) < 0) |
| | | { |
| | | sortValue = v; |
| | | } |
| | | } |
| | | } |
| | | |
| | | values[i] = sortValue; |
| | | } |
| | | } |
| | | return values; |
| | | } |
| | | |
| | | /** |
| | | * Encode a VLV database key with the given information. |
| | | * |
| | | * @param entryID The entry ID to encode. |
| | | * @param values The values to encode. |
| | | * @param types The types of the values to encode. |
| | | * @return The encoded bytes. |
| | | * @throws DirectoryException If a Directory Server error occurs. |
| | | */ |
| | | ByteString encodeKey(long entryID, ByteString[] values, AttributeType[] types) |
| | | throws DirectoryException |
| | | { |
| | | try |
| | | { |
| | | final ByteStringBuilder builder = new ByteStringBuilder(); |
| | | |
| | | for (int i = 0; i < values.length; i++) |
| | | { |
| | | final ByteString v = values[i]; |
| | | if (v == null) |
| | | { |
| | | builder.appendBERLength(0); |
| | | } |
| | | else |
| | | { |
| | | final MatchingRule eqRule = types[i].getEqualityMatchingRule(); |
| | | final ByteString nv = eqRule.normalizeAttributeValue(v); |
| | | builder.appendBERLength(nv.length()); |
| | | builder.append(nv); |
| | | } |
| | | } |
| | | builder.append(entryID); |
| | | builder.trimToSize(); |
| | | |
| | | return builder.toByteString(); |
| | | } |
| | | catch (DecodeException e) |
| | | { |
| | | throw new DirectoryException( |
| | | ResultCode.INVALID_ATTRIBUTE_SYNTAX, e.getMessageObject(), e); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Decode a VLV database key. |
| | | * |
| | | * @param keyBytes The byte array to decode. |
| | | * @return The sort values represented by the key bytes. |
| | | * @throws DirectoryException If a Directory Server error occurs. |
| | | */ |
| | | private SortValues decodeKey(ByteString keyBytes) throws DirectoryException |
| | | { |
| | | if(keyBytes == null || keyBytes.length() == 0) |
| | | { |
| | | return null; |
| | | } |
| | | |
| | | ByteString[] attributeValues = new ByteString[sortOrder.getSortKeys().length]; |
| | | int vBytesPos = 0; |
| | | |
| | | for(int i = 0; i < attributeValues.length; i++) |
| | | { |
| | | int valueLength = keyBytes.byteAt(vBytesPos) & 0x7F; |
| | | if (valueLength != keyBytes.byteAt(vBytesPos++)) |
| | | { |
| | | int valueLengthBytes = valueLength; |
| | | valueLength = 0; |
| | | for (int j=0; j < valueLengthBytes; j++, vBytesPos++) |
| | | { |
| | | valueLength = (valueLength << 8) | (keyBytes.byteAt(vBytesPos) & 0xFF); |
| | | } |
| | | } |
| | | |
| | | if(valueLength == 0) |
| | | { |
| | | attributeValues[i] = null; |
| | | } |
| | | else |
| | | { |
| | | byte[] valueBytes = new byte[valueLength]; |
| | | System.arraycopy(keyBytes, vBytesPos, valueBytes, 0, valueLength); |
| | | attributeValues[i] = ByteString.wrap(valueBytes); |
| | | } |
| | | |
| | | vBytesPos += valueLength; |
| | | } |
| | | |
| | | final long id = JebFormat.toLong(keyBytes.toByteArray(), vBytesPos, keyBytes.length()); |
| | | return new SortValues(new EntryID(id), attributeValues, sortOrder); |
| | | } |
| | | |
| | | /** |
| | | * Get the sorted set capacity configured for this VLV index. |
| | | * |
| | | * @return The sorted set capacity. |
| | | */ |
| | | public int getSortedSetCapacity() |
| | | { |
| | | return sortedSetCapacity; |
| | | } |
| | | |
| | | /** |
| | | * Indicates if the given entry should belong in this VLV index. |
| | | * |
| | | * @param entry The entry to check. |
| | | * @return True if the given entry should belong in this VLV index or False |
| | | * otherwise. |
| | | * @throws DirectoryException If a Directory Server error occurs. |
| | | */ |
| | | public boolean shouldInclude(Entry entry) throws DirectoryException |
| | | { |
| | | DN entryDN = entry.getName(); |
| | | return entryDN.matchesBaseAndScope(baseDN, scope) |
| | | && filter.matchesEntry(entry); |
| | | } |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public synchronized boolean isConfigurationChangeAcceptable( |
| | | LocalDBVLVIndexCfg cfg, |
| | | List<LocalizableMessage> unacceptableReasons) |
| | | { |
| | | try |
| | | { |
| | | this.filter = SearchFilter.createFilterFromString(cfg.getFilter()); |
| | | } |
| | | catch(Exception e) |
| | | { |
| | | LocalizableMessage msg = ERR_JEB_CONFIG_VLV_INDEX_BAD_FILTER.get( |
| | | cfg.getFilter(), treeName, |
| | | stackTraceToSingleLineString(e)); |
| | | unacceptableReasons.add(msg); |
| | | return false; |
| | | } |
| | | |
| | | String[] sortAttrs = cfg.getSortOrder().split(" "); |
| | | SortKey[] sortKeys = new SortKey[sortAttrs.length]; |
| | | MatchingRule[] orderingRules = new MatchingRule[sortAttrs.length]; |
| | | boolean[] ascending = new boolean[sortAttrs.length]; |
| | | for(int i = 0; i < sortAttrs.length; i++) |
| | | { |
| | | try |
| | | { |
| | | if(sortAttrs[i].startsWith("-")) |
| | | { |
| | | ascending[i] = false; |
| | | sortAttrs[i] = sortAttrs[i].substring(1); |
| | | } |
| | | else |
| | | { |
| | | ascending[i] = true; |
| | | if(sortAttrs[i].startsWith("+")) |
| | | { |
| | | sortAttrs[i] = sortAttrs[i].substring(1); |
| | | } |
| | | } |
| | | } |
| | | catch(Exception e) |
| | | { |
| | | unacceptableReasons.add(ERR_JEB_CONFIG_VLV_INDEX_UNDEFINED_ATTR.get(sortKeys[i], treeName)); |
| | | return false; |
| | | } |
| | | |
| | | AttributeType attrType = DirectoryServer.getAttributeType(sortAttrs[i].toLowerCase()); |
| | | if(attrType == null) |
| | | { |
| | | LocalizableMessage msg = ERR_JEB_CONFIG_VLV_INDEX_UNDEFINED_ATTR.get(sortAttrs[i], treeName); |
| | | unacceptableReasons.add(msg); |
| | | return false; |
| | | } |
| | | sortKeys[i] = new SortKey(attrType, ascending[i]); |
| | | orderingRules[i] = attrType.getOrderingMatchingRule(); |
| | | } |
| | | |
| | | return true; |
| | | } |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public synchronized ConfigChangeResult applyConfigurationChange( |
| | | LocalDBVLVIndexCfg cfg) |
| | | { |
| | | ResultCode resultCode = ResultCode.SUCCESS; |
| | | boolean adminActionRequired = false; |
| | | ArrayList<LocalizableMessage> messages = new ArrayList<LocalizableMessage>(); |
| | | |
| | | // Update base DN only if changed.. |
| | | if(!config.getBaseDN().equals(cfg.getBaseDN())) |
| | | { |
| | | this.baseDN = cfg.getBaseDN(); |
| | | adminActionRequired = true; |
| | | } |
| | | |
| | | // Update scope only if changed. |
| | | if(!config.getScope().equals(cfg.getScope())) |
| | | { |
| | | this.scope = SearchScope.valueOf(cfg.getScope().name()); |
| | | adminActionRequired = true; |
| | | } |
| | | |
| | | // Update sort set capacity only if changed. |
| | | if (config.getMaxBlockSize() != cfg.getMaxBlockSize()) |
| | | { |
| | | this.sortedSetCapacity = cfg.getMaxBlockSize(); |
| | | |
| | | // Require admin action only if the new capacity is larger. Otherwise, |
| | | // we will lazyly update the sorted sets. |
| | | if (config.getMaxBlockSize() < cfg.getMaxBlockSize()) |
| | | { |
| | | adminActionRequired = true; |
| | | } |
| | | } |
| | | |
| | | // Update the filter only if changed. |
| | | if(!config.getFilter().equals(cfg.getFilter())) |
| | | { |
| | | try |
| | | { |
| | | this.filter = SearchFilter.createFilterFromString(cfg.getFilter()); |
| | | adminActionRequired = true; |
| | | } |
| | | catch(Exception e) |
| | | { |
| | | LocalizableMessage msg = ERR_JEB_CONFIG_VLV_INDEX_BAD_FILTER.get( |
| | | config.getFilter(), treeName, |
| | | stackTraceToSingleLineString(e)); |
| | | messages.add(msg); |
| | | if(resultCode == ResultCode.SUCCESS) |
| | | { |
| | | resultCode = ResultCode.INVALID_ATTRIBUTE_SYNTAX; |
| | | } |
| | | } |
| | | } |
| | | |
| | | // Update the sort order only if changed. |
| | | if (!config.getSortOrder().equals(cfg.getSortOrder())) |
| | | { |
| | | String[] sortAttrs = cfg.getSortOrder().split(" "); |
| | | SortKey[] sortKeys = new SortKey[sortAttrs.length]; |
| | | MatchingRule[] orderingRules = new MatchingRule[sortAttrs.length]; |
| | | boolean[] ascending = new boolean[sortAttrs.length]; |
| | | for(int i = 0; i < sortAttrs.length; i++) |
| | | { |
| | | try |
| | | { |
| | | if(sortAttrs[i].startsWith("-")) |
| | | { |
| | | ascending[i] = false; |
| | | sortAttrs[i] = sortAttrs[i].substring(1); |
| | | } |
| | | else |
| | | { |
| | | ascending[i] = true; |
| | | if(sortAttrs[i].startsWith("+")) |
| | | { |
| | | sortAttrs[i] = sortAttrs[i].substring(1); |
| | | } |
| | | } |
| | | } |
| | | catch(Exception e) |
| | | { |
| | | messages.add(ERR_JEB_CONFIG_VLV_INDEX_UNDEFINED_ATTR.get(sortKeys[i], treeName)); |
| | | if(resultCode == ResultCode.SUCCESS) |
| | | { |
| | | resultCode = ResultCode.INVALID_ATTRIBUTE_SYNTAX; |
| | | } |
| | | } |
| | | |
| | | AttributeType attrType = |
| | | DirectoryServer.getAttributeType(sortAttrs[i].toLowerCase()); |
| | | if(attrType == null) |
| | | { |
| | | messages.add(ERR_JEB_CONFIG_VLV_INDEX_UNDEFINED_ATTR.get(sortKeys[i], treeName)); |
| | | if(resultCode == ResultCode.SUCCESS) |
| | | { |
| | | resultCode = ResultCode.INVALID_ATTRIBUTE_SYNTAX; |
| | | } |
| | | } |
| | | else |
| | | { |
| | | sortKeys[i] = new SortKey(attrType, ascending[i]); |
| | | orderingRules[i] = attrType.getOrderingMatchingRule(); |
| | | } |
| | | } |
| | | |
| | | this.sortOrder = new SortOrder(sortKeys); |
| | | this.comparator = new VLVKeyComparator(orderingRules, ascending); |
| | | |
| | | // We have to close the database and open it using the new comparator. |
| | | entryContainer.exclusiveLock.lock(); |
| | | try |
| | | { |
| | | close(); |
| | | open(); |
| | | } |
| | | catch(StorageRuntimeException de) |
| | | { |
| | | messages.add(LocalizableMessage.raw(StaticUtils.stackTraceToSingleLineString(de))); |
| | | if(resultCode == ResultCode.SUCCESS) |
| | | { |
| | | resultCode = DirectoryServer.getServerErrorResultCode(); |
| | | } |
| | | } |
| | | finally |
| | | { |
| | | entryContainer.exclusiveLock.unlock(); |
| | | } |
| | | |
| | | adminActionRequired = true; |
| | | } |
| | | |
| | | |
| | | if(adminActionRequired) |
| | | { |
| | | trusted = false; |
| | | messages.add(NOTE_JEB_INDEX_ADD_REQUIRES_REBUILD.get(treeName)); |
| | | try |
| | | { |
| | | state.putIndexTrustState(null, this, false); |
| | | } |
| | | catch(StorageRuntimeException de) |
| | | { |
| | | messages.add(LocalizableMessage.raw(StaticUtils.stackTraceToSingleLineString(de))); |
| | | if(resultCode == ResultCode.SUCCESS) |
| | | { |
| | | resultCode = DirectoryServer.getServerErrorResultCode(); |
| | | } |
| | | } |
| | | } |
| | | |
| | | this.config = cfg; |
| | | return new ConfigChangeResult(resultCode, adminActionRequired, messages); |
| | | } |
| | | } |
| New file |
| | |
| | | /* |
| | | * CDDL HEADER START |
| | | * |
| | | * The contents of this file are subject to the terms of the |
| | | * Common Development and Distribution License, Version 1.0 only |
| | | * (the "License"). You may not use this file except in compliance |
| | | * with the License. |
| | | * |
| | | * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt |
| | | * or http://forgerock.org/license/CDDLv1.0.html. |
| | | * See the License for the specific language governing permissions |
| | | * and limitations under the License. |
| | | * |
| | | * When distributing Covered Code, include this CDDL HEADER in each |
| | | * file and include the License file at legal-notices/CDDLv1_0.txt. |
| | | * If applicable, add the following below this CDDL HEADER, with the |
| | | * fields enclosed by brackets "[]" replaced with your own identifying |
| | | * information: |
| | | * Portions Copyright [yyyy] [name of copyright owner] |
| | | * |
| | | * CDDL HEADER END |
| | | * |
| | | * |
| | | * Copyright 2006-2008 Sun Microsystems, Inc. |
| | | * Portions Copyright 2014 ForgeRock AS |
| | | */ |
| | | package org.opends.server.backends.pluggable; |
| | | |
| | | import java.util.Comparator; |
| | | |
| | | import org.forgerock.opendj.ldap.ByteSequence; |
| | | import org.forgerock.opendj.ldap.ByteString; |
| | | import org.forgerock.opendj.ldap.DecodeException; |
| | | import org.forgerock.opendj.ldap.ResultCode; |
| | | import org.forgerock.opendj.ldap.schema.MatchingRule; |
| | | import org.opends.server.backends.pluggable.BackendImpl.StorageRuntimeException; |
| | | import org.opends.server.core.DirectoryServer; |
| | | import org.opends.server.types.DirectoryException; |
| | | |
| | | /** |
| | | * This class is used to compare the keys used in a VLV index. Each key is |
| | | * made up the sort values and the entry ID of the largest entry in the sorted |
| | | * set stored in the data for the key. |
| | | */ |
| | | public class VLVKeyComparator implements DatabaseComparator |
| | | { |
| | | /** |
| | | * The serial version identifier required to satisfy the compiler because this |
| | | * class implements the <CODE>java.io.Serializable</CODE> interface. This |
| | | * value was generated using the <CODE>serialver</CODE> command-line utility |
| | | * included with the Java SDK. |
| | | */ |
| | | static final long serialVersionUID = 1585167927344130604L; |
| | | |
| | | /** Matching rules are not serializable. */ |
| | | private transient MatchingRule[] orderingRules; |
| | | |
| | | /** |
| | | * Only oids of matching rules are recorded for serialization. Oids allow to |
| | | * retrieve matching rules after deserialization, through |
| | | * {@code initialize(ClassLoader)} method. |
| | | */ |
| | | private String[] orderingRuleOids; |
| | | |
| | | private boolean[] ascending; |
| | | |
| | | /** |
| | | * Construct a new VLV Key Comparator object. |
| | | * |
| | | * @param orderingRules The array of ordering rules to use when comparing |
| | | * the decoded values in the key. |
| | | * @param ascending The array of booleans indicating the ordering for |
| | | * each value. |
| | | */ |
| | | public VLVKeyComparator(MatchingRule[] orderingRules, boolean[] ascending) |
| | | { |
| | | this.orderingRules = orderingRules; |
| | | this.orderingRuleOids = new String[orderingRules.length]; |
| | | for (int i = 0; i < orderingRules.length; i++) |
| | | { |
| | | orderingRuleOids[i] = orderingRules[i].getOID(); |
| | | } |
| | | this.ascending = ascending; |
| | | } |
| | | |
| | | /** |
| | | * Compares the contents of the provided byte arrays to determine their |
| | | * relative order. A key in the VLV index contains the sorted attribute values |
| | | * in order followed by the 8 byte entry ID. A attribute value of length 0 |
| | | * means that value is null and the attribute type was not part of the entry. |
| | | * A null value is always considered greater then a non null value. If all |
| | | * attribute values are the same, the entry ID will be used to determine the |
| | | * ordering. |
| | | * |
| | | * When comparing partial keys (ie. keys with only the first attribute value |
| | | * encoded for evaluating VLV assertion value offsets or keys with no entry |
| | | * IDs), only information available in both byte keys will be used to |
| | | * determine the ordering. If all available information is the same, 0 will |
| | | * be returned. |
| | | * |
| | | * @param b1 The first byte array to use in the comparison. |
| | | * @param b2 The second byte array to use in the comparison. |
| | | * |
| | | * @return A negative integer if <CODE>b1</CODE> should come before |
| | | * <CODE>b2</CODE> in ascending order, a positive integer if |
| | | * <CODE>b1</CODE> should come after <CODE>b2</CODE> in ascending |
| | | * order, or zero if there is no difference between the values with |
| | | * regard to ordering. |
| | | */ |
| | | @Override |
| | | public int compare(byte[] b1, byte[] b2) |
| | | { |
| | | // A 0 length byte array is a special key used for the unbound max |
| | | // sort values set. It always comes after a non length byte array. |
| | | if(b1.length == 0) |
| | | { |
| | | if(b2.length == 0) |
| | | { |
| | | return 0; |
| | | } |
| | | else |
| | | { |
| | | return 1; |
| | | } |
| | | } |
| | | else if(b2.length == 0) |
| | | { |
| | | return -1; |
| | | } |
| | | |
| | | int b1Pos = 0; |
| | | int b2Pos = 0; |
| | | for (int j=0; |
| | | j < orderingRules.length && b1Pos < b1.length && b2Pos < b2.length; |
| | | j++) |
| | | { |
| | | int b1Length = b1[b1Pos] & 0x7F; |
| | | if (b1[b1Pos++] != b1Length) |
| | | { |
| | | int b1NumLengthBytes = b1Length; |
| | | b1Length = 0; |
| | | for (int k=0; k < b1NumLengthBytes; k++, b1Pos++) |
| | | { |
| | | b1Length = (b1Length << 8) | |
| | | (b1[b1Pos] & 0xFF); |
| | | } |
| | | } |
| | | |
| | | int b2Length = b2[b2Pos] & 0x7F; |
| | | if (b2[b2Pos++] != b2Length) |
| | | { |
| | | int b2NumLengthBytes = b2Length; |
| | | b2Length = 0; |
| | | for (int k=0; k < b2NumLengthBytes; k++, b2Pos++) |
| | | { |
| | | b2Length = (b2Length << 8) | |
| | | (b2[b2Pos] & 0xFF); |
| | | } |
| | | } |
| | | |
| | | byte[] b1Bytes; |
| | | byte[] b2Bytes; |
| | | if(b1Length > 0) |
| | | { |
| | | b1Bytes = new byte[b1Length]; |
| | | System.arraycopy(b1, b1Pos, b1Bytes, 0, b1Length); |
| | | b1Pos += b1Length; |
| | | } |
| | | else |
| | | { |
| | | b1Bytes = null; |
| | | } |
| | | |
| | | if(b2Length > 0) |
| | | { |
| | | b2Bytes = new byte[b2Length]; |
| | | System.arraycopy(b2, b2Pos, b2Bytes, 0, b2Length); |
| | | b2Pos += b2Length; |
| | | } |
| | | else |
| | | { |
| | | b2Bytes = null; |
| | | } |
| | | |
| | | // A null value will always come after a non-null value. |
| | | if (b1Bytes == null) |
| | | { |
| | | if (b2Bytes == null) |
| | | { |
| | | continue; |
| | | } |
| | | else |
| | | { |
| | | return 1; |
| | | } |
| | | } |
| | | else if (b2Bytes == null) |
| | | { |
| | | return -1; |
| | | } |
| | | |
| | | final Comparator<ByteSequence> comp = orderingRules[j].comparator(); |
| | | final ByteString val1 = ByteString.valueOf(b1Bytes); |
| | | final ByteString val2 = ByteString.valueOf(b2Bytes); |
| | | final int result = ascending[j] ? comp.compare(val1, val2) : comp.compare(val2, val1); |
| | | |
| | | if(result != 0) |
| | | { |
| | | return result; |
| | | } |
| | | } |
| | | |
| | | // If we've gotten here, then we can't tell a difference between the sets |
| | | // of available values, so sort based on entry ID if its in the key. |
| | | |
| | | if(b1Pos + 8 <= b1.length && b2Pos + 8 <= b2.length) |
| | | { |
| | | long b1ID = JebFormat.toLong(b1, b1Pos, b1Pos + 8); |
| | | long b2ID = JebFormat.toLong(b2, b2Pos, b2Pos + 8); |
| | | return compare(b1ID, b2ID); |
| | | } |
| | | |
| | | // If we've gotten here, then we can't tell the difference between the sets |
| | | // of available values and entry IDs are not all available, so just return 0 |
| | | return 0; |
| | | } |
| | | |
| | | /** |
| | | * Compares the contents in the provided values set with the given values to |
| | | * determine their relative order. A null value is always considered greater |
| | | * then a non null value. If all attribute values are the same, the entry ID |
| | | * will be used to determine the ordering. |
| | | * |
| | | * If the given attribute values array does not contain all the values in the |
| | | * sort order, any missing values will be considered as a unknown or |
| | | * wildcard value instead of a non existent value. When comparing partial |
| | | * information, only values available in both the values set and the |
| | | * given values will be used to determine the ordering. If all available |
| | | * information is the same, 0 will be returned. |
| | | * |
| | | * @param set The sort values set to containing the values. |
| | | * @param index The index of the values in the set. |
| | | * @param entryID The entry ID to use in the comparison. |
| | | * @param values The values to use in the comparison. |
| | | * @return A negative integer if the values in the set should come before |
| | | * the given values in ascending order, a positive integer if |
| | | * the values in the set should come after the given values in |
| | | * ascending order, or zero if there is no difference between the |
| | | * values with regard to ordering. |
| | | * @throws StorageRuntimeException If an error occurs during an operation on a |
| | | * JE database. |
| | | * @throws DirectoryException If an error occurs while trying to |
| | | * normalize the value (e.g., if it is |
| | | * not acceptable for use with the |
| | | * associated equality matching rule). |
| | | */ |
| | | public int compare(SortValuesSet set, int index, long entryID, |
| | | ByteSequence... values) throws StorageRuntimeException, DirectoryException |
| | | { |
| | | for (int j=0; j < orderingRules.length; j++) |
| | | { |
| | | if(j >= values.length) |
| | | { |
| | | break; |
| | | } |
| | | |
| | | ByteString b1Bytes = set.getValue((index * orderingRules.length) + j); |
| | | ByteString b2Bytes = null; |
| | | |
| | | if(values[j] != null) |
| | | { |
| | | try |
| | | { |
| | | b2Bytes = orderingRules[j].normalizeAttributeValue(values[j]); |
| | | } |
| | | catch (DecodeException e) |
| | | { |
| | | throw new DirectoryException( |
| | | ResultCode.INVALID_ATTRIBUTE_SYNTAX, e.getMessageObject(), e); |
| | | } |
| | | } |
| | | |
| | | // A null value will always come after a non-null value. |
| | | if (b1Bytes == null) |
| | | { |
| | | if (b2Bytes == null) |
| | | { |
| | | continue; |
| | | } |
| | | else |
| | | { |
| | | return 1; |
| | | } |
| | | } |
| | | else if (b2Bytes == null) |
| | | { |
| | | return -1; |
| | | } |
| | | |
| | | final Comparator<ByteSequence> comp = orderingRules[j].comparator(); |
| | | final int result = ascending[j] ? comp.compare(b1Bytes, b2Bytes) : comp.compare(b2Bytes, b1Bytes); |
| | | |
| | | if(result != 0) |
| | | { |
| | | return result; |
| | | } |
| | | } |
| | | |
| | | if(entryID != -1) |
| | | { |
| | | // If we've gotten here, then we can't tell a difference between the sets |
| | | // of values, so sort based on entry ID. |
| | | return compare(set.getEntryIDs()[index], entryID); |
| | | } |
| | | |
| | | // If we've gotten here, then we can't tell the difference between the sets |
| | | // of available values and the entry ID is not available. Just return 0. |
| | | return 0; |
| | | } |
| | | |
| | | private int compare(long l1, long l2) |
| | | { |
| | | final long difference = l1 - l2; |
| | | if (difference < 0) |
| | | { |
| | | return -1; |
| | | } |
| | | else if (difference > 0) |
| | | { |
| | | return 1; |
| | | } |
| | | else |
| | | { |
| | | return 0; |
| | | } |
| | | } |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public void initialize(ClassLoader loader) |
| | | { |
| | | if (orderingRules == null) |
| | | { |
| | | orderingRules = new MatchingRule[orderingRuleOids.length]; |
| | | for (int i = 0; i < orderingRuleOids.length; i++) |
| | | { |
| | | orderingRules[i] = DirectoryServer.getSchema().getMatchingRule(orderingRuleOids[i]); |
| | | } |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | /* |
| | | * CDDL HEADER START |
| | | * |
| | | * The contents of this file are subject to the terms of the |
| | | * Common Development and Distribution License, Version 1.0 only |
| | | * (the "License"). You may not use this file except in compliance |
| | | * with the License. |
| | | * |
| | | * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt |
| | | * or http://forgerock.org/license/CDDLv1.0.html. |
| | | * See the License for the specific language governing permissions |
| | | * and limitations under the License. |
| | | * |
| | | * When distributing Covered Code, include this CDDL HEADER in each |
| | | * file and include the License file at legal-notices/CDDLv1_0.txt. |
| | | * If applicable, add the following below this CDDL HEADER, with the |
| | | * fields enclosed by brackets "[]" replaced with your own identifying |
| | | * information: |
| | | * Portions Copyright [yyyy] [name of copyright owner] |
| | | * |
| | | * CDDL HEADER END |
| | | * |
| | | * |
| | | * Copyright 2006-2008 Sun Microsystems, Inc. |
| | | * Portions Copyright 2014 ForgeRock AS |
| | | */ |
| | | package org.opends.server.backends.pluggable; |
| | | |
| | | import org.opends.server.types.DN; |
| | | |
| | | import java.util.ArrayList; |
| | | |
| | | /** |
| | | * This class represents the configuration of a JE backend verification process. |
| | | */ |
| | | public class VerifyConfig |
| | | { |
| | | /** |
| | | * The base DN to be verified. |
| | | */ |
| | | private DN baseDN; |
| | | |
| | | /** |
| | | * The names of indexes to be verified for completeness. |
| | | */ |
| | | private ArrayList<String> completeList; |
| | | |
| | | /** |
| | | * The names of indexes to be verified for cleanliness. |
| | | */ |
| | | private ArrayList<String> cleanList; |
| | | |
| | | /** |
| | | * Create a new verify configuration. |
| | | */ |
| | | public VerifyConfig() |
| | | { |
| | | baseDN = null; |
| | | completeList = new ArrayList<String>(); |
| | | cleanList = new ArrayList<String>(); |
| | | } |
| | | |
| | | /** |
| | | * Get the base DN to be verified. |
| | | * @return The base DN to be verified. |
| | | */ |
| | | public DN getBaseDN() |
| | | { |
| | | return baseDN; |
| | | } |
| | | |
| | | /** |
| | | * Set the base DN to be verified. |
| | | * @param baseDN The base DN to be verified. |
| | | */ |
| | | public void setBaseDN(DN baseDN) |
| | | { |
| | | this.baseDN = baseDN; |
| | | } |
| | | |
| | | /** |
| | | * Get the names of indexes to be verified for completeness. |
| | | * @return The names of indexes to be verified for completeness. |
| | | */ |
| | | public ArrayList<String> getCompleteList() |
| | | { |
| | | return completeList; |
| | | } |
| | | |
| | | /** |
| | | * Add the name of an index to those indexes to be verified for completeness. |
| | | * @param index The name of an index to be verified for completeness. |
| | | */ |
| | | public void addCompleteIndex(String index) |
| | | { |
| | | completeList.add(index); |
| | | } |
| | | |
| | | /** |
| | | * Get the names of indexes to be verified for cleanliness. |
| | | * @return The names of indexes to be verified for cleanliness. |
| | | */ |
| | | public ArrayList<String> getCleanList() |
| | | { |
| | | return cleanList; |
| | | } |
| | | |
| | | /** |
| | | * Add the name of an index to those indexes to be verified for cleanliness. |
| | | * @param index The name of an index to be verified for cleanliness. |
| | | */ |
| | | public void addCleanIndex(String index) |
| | | { |
| | | cleanList.add(index); |
| | | } |
| | | } |
| New file |
| | |
| | | /* |
| | | * CDDL HEADER START |
| | | * |
| | | * The contents of this file are subject to the terms of the |
| | | * Common Development and Distribution License, Version 1.0 only |
| | | * (the "License"). You may not use this file except in compliance |
| | | * with the License. |
| | | * |
| | | * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt |
| | | * or http://forgerock.org/license/CDDLv1.0.html. |
| | | * See the License for the specific language governing permissions |
| | | * and limitations under the License. |
| | | * |
| | | * When distributing Covered Code, include this CDDL HEADER in each |
| | | * file and include the License file at legal-notices/CDDLv1_0.txt. |
| | | * If applicable, add the following below this CDDL HEADER, with the |
| | | * fields enclosed by brackets "[]" replaced with your own identifying |
| | | * information: |
| | | * Portions Copyright [yyyy] [name of copyright owner] |
| | | * |
| | | * CDDL HEADER END |
| | | * |
| | | * |
| | | * Copyright 2006-2010 Sun Microsystems, Inc. |
| | | * Portions Copyright 2011-2014 ForgeRock AS |
| | | */ |
| | | package org.opends.server.backends.pluggable; |
| | | import java.util.AbstractSet; |
| | | import java.util.ArrayList; |
| | | import java.util.Arrays; |
| | | import java.util.Collections; |
| | | import java.util.HashMap; |
| | | import java.util.IdentityHashMap; |
| | | import java.util.Iterator; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import java.util.Set; |
| | | import java.util.Timer; |
| | | import java.util.TimerTask; |
| | | import java.util.concurrent.atomic.AtomicBoolean; |
| | | |
| | | import org.forgerock.i18n.LocalizableMessage; |
| | | import org.forgerock.i18n.slf4j.LocalizedLogger; |
| | | import org.forgerock.opendj.ldap.ByteSequence; |
| | | import org.forgerock.opendj.ldap.ByteString; |
| | | import org.forgerock.opendj.ldap.ConditionResult; |
| | | import org.forgerock.opendj.ldap.DecodeException; |
| | | import org.forgerock.opendj.ldap.ResultCode; |
| | | import org.forgerock.opendj.ldap.schema.MatchingRule; |
| | | import org.forgerock.opendj.ldap.spi.IndexingOptions; |
| | | import org.opends.server.backends.pluggable.BackendImpl.Cursor; |
| | | import org.opends.server.backends.pluggable.BackendImpl.ReadOperation; |
| | | import org.opends.server.backends.pluggable.BackendImpl.ReadableStorage; |
| | | import org.opends.server.backends.pluggable.BackendImpl.Storage; |
| | | import org.opends.server.backends.pluggable.BackendImpl.StorageRuntimeException; |
| | | import org.opends.server.core.DirectoryServer; |
| | | import org.opends.server.types.Attribute; |
| | | import org.opends.server.types.AttributeType; |
| | | import org.opends.server.types.Attributes; |
| | | import org.opends.server.types.DN; |
| | | import org.opends.server.types.DirectoryException; |
| | | import org.opends.server.types.Entry; |
| | | import org.opends.server.util.ServerConstants; |
| | | import org.opends.server.util.StaticUtils; |
| | | |
| | | import com.sleepycat.je.EnvironmentStats; |
| | | import com.sleepycat.je.StatsConfig; |
| | | |
| | | import static org.opends.messages.JebMessages.*; |
| | | import static org.opends.server.backends.pluggable.JebFormat.*; |
| | | |
| | | /** |
| | | * This class is used to run an index verification process on the backend. |
| | | */ |
| | | public class VerifyJob |
| | | { |
| | | private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); |
| | | |
| | | /** The verify configuration. */ |
| | | private final VerifyConfig verifyConfig; |
| | | /** The root container used for the verify job. */ |
| | | private RootContainer rootContainer; |
| | | |
| | | /** The number of milliseconds between job progress reports. */ |
| | | private final long progressInterval = 10000; |
| | | /** The number of index keys processed. */ |
| | | private long keyCount; |
| | | /** The number of errors found. */ |
| | | private long errorCount; |
| | | /** The number of records that have exceeded the entry limit. */ |
| | | private long entryLimitExceededCount; |
| | | /** The number of records that reference more than one entry. */ |
| | | private long multiReferenceCount; |
| | | /** The total number of entry references. */ |
| | | private long entryReferencesCount; |
| | | /** The maximum number of references per record. */ |
| | | private long maxEntryPerValue; |
| | | |
| | | /** |
| | | * This map is used to gather some statistics about values that have |
| | | * exceeded the entry limit. |
| | | */ |
| | | private IdentityHashMap<Index, HashMap<ByteString, Long>> entryLimitMap = |
| | | new IdentityHashMap<Index, HashMap<ByteString, Long>>(); |
| | | |
| | | /** Indicates whether the DN database is to be verified. */ |
| | | private boolean verifyDN2ID; |
| | | /** Indicates whether the children database is to be verified. */ |
| | | private boolean verifyID2Children; |
| | | /** Indicates whether the subtree database is to be verified. */ |
| | | private boolean verifyID2Subtree; |
| | | |
| | | /** The entry database. */ |
| | | private ID2Entry id2entry; |
| | | /** The DN database. */ |
| | | private DN2ID dn2id; |
| | | /** The children database. */ |
| | | private Index id2c; |
| | | /** The subtree database. */ |
| | | private Index id2s; |
| | | |
| | | /** |
| | | * A list of the attribute indexes to be verified. |
| | | */ |
| | | private final ArrayList<AttributeIndex> attrIndexList = new ArrayList<AttributeIndex>(); |
| | | |
| | | /** |
| | | * A list of the VLV indexes to be verified. |
| | | */ |
| | | private final ArrayList<VLVIndex> vlvIndexList = new ArrayList<VLVIndex>(); |
| | | |
| | | /** |
| | | * Construct a VerifyJob. |
| | | * |
| | | * @param verifyConfig The verify configuration. |
| | | */ |
| | | public VerifyJob(VerifyConfig verifyConfig) |
| | | { |
| | | this.verifyConfig = verifyConfig; |
| | | } |
| | | |
| | | /** |
| | | * Verify the backend. |
| | | * |
| | | * @param rootContainer The root container that holds the entries to verify. |
| | | * @param statEntry Optional statistics entry. |
| | | * @return The error count. |
| | | * @throws StorageRuntimeException If an error occurs in the JE database. |
| | | * @throws JebException If an error occurs in the JE backend. |
| | | * @throws DirectoryException If an error occurs while verifying the backend. |
| | | */ |
| | | public long verifyBackend(final RootContainer rootContainer, final Entry statEntry) throws StorageRuntimeException, |
| | | JebException, DirectoryException |
| | | { |
| | | Storage s; |
| | | try |
| | | { |
| | | return s.read(new ReadOperation<Long>() |
| | | { |
| | | @Override |
| | | public Long run(ReadableStorage txn) throws Exception |
| | | { |
| | | return verifyBackend0(txn, rootContainer, statEntry); |
| | | } |
| | | }); |
| | | } |
| | | catch (Exception e) |
| | | { |
| | | throw new StorageRuntimeException(e); |
| | | } |
| | | } |
| | | |
| | | private long verifyBackend0(ReadableStorage txn, RootContainer rootContainer, Entry statEntry) |
| | | throws StorageRuntimeException, JebException, DirectoryException |
| | | { |
| | | this.rootContainer = rootContainer; |
| | | EntryContainer entryContainer = |
| | | rootContainer.getEntryContainer(verifyConfig.getBaseDN()); |
| | | |
| | | entryContainer.sharedLock.lock(); |
| | | try |
| | | { |
| | | ArrayList<String> completeList = verifyConfig.getCompleteList(); |
| | | ArrayList<String> cleanList = verifyConfig.getCleanList(); |
| | | |
| | | boolean cleanMode = false; |
| | | if (completeList.isEmpty() && cleanList.isEmpty()) |
| | | { |
| | | verifyDN2ID = true; |
| | | if (rootContainer.getConfiguration().isSubordinateIndexesEnabled()) |
| | | { |
| | | verifyID2Children = true; |
| | | verifyID2Subtree = true; |
| | | } |
| | | attrIndexList.addAll(entryContainer.getAttributeIndexes()); |
| | | } |
| | | else |
| | | { |
| | | ArrayList<String> list; |
| | | if (!completeList.isEmpty()) |
| | | { |
| | | list = completeList; |
| | | } |
| | | else |
| | | { |
| | | list = cleanList; |
| | | cleanMode = true; |
| | | } |
| | | |
| | | for (String index : list) |
| | | { |
| | | String lowerName = index.toLowerCase(); |
| | | if ("dn2id".equals(lowerName)) |
| | | { |
| | | verifyDN2ID = true; |
| | | } |
| | | else if ("id2children".equals(lowerName)) |
| | | { |
| | | if (rootContainer.getConfiguration().isSubordinateIndexesEnabled()) |
| | | { |
| | | verifyID2Children = true; |
| | | } |
| | | else |
| | | { |
| | | LocalizableMessage msg = NOTE_JEB_SUBORDINATE_INDEXES_DISABLED |
| | | .get(rootContainer.getConfiguration().getBackendId()); |
| | | throw new JebException(msg); |
| | | } |
| | | } |
| | | else if ("id2subtree".equals(lowerName)) |
| | | { |
| | | if (rootContainer.getConfiguration().isSubordinateIndexesEnabled()) |
| | | { |
| | | verifyID2Subtree = true; |
| | | } |
| | | else |
| | | { |
| | | LocalizableMessage msg = NOTE_JEB_SUBORDINATE_INDEXES_DISABLED |
| | | .get(rootContainer.getConfiguration().getBackendId()); |
| | | throw new JebException(msg); |
| | | } |
| | | } |
| | | else if(lowerName.startsWith("vlv.")) |
| | | { |
| | | if(lowerName.length() < 5) |
| | | { |
| | | LocalizableMessage msg = ERR_JEB_VLV_INDEX_NOT_CONFIGURED.get(lowerName); |
| | | throw new JebException(msg); |
| | | } |
| | | |
| | | VLVIndex vlvIndex = |
| | | entryContainer.getVLVIndex(lowerName.substring(4)); |
| | | if(vlvIndex == null) |
| | | { |
| | | LocalizableMessage msg = |
| | | ERR_JEB_VLV_INDEX_NOT_CONFIGURED.get(lowerName.substring(4)); |
| | | throw new JebException(msg); |
| | | } |
| | | |
| | | vlvIndexList.add(vlvIndex); |
| | | } |
| | | else |
| | | { |
| | | AttributeType attrType = |
| | | DirectoryServer.getAttributeType(lowerName); |
| | | if (attrType == null) |
| | | { |
| | | LocalizableMessage msg = ERR_JEB_ATTRIBUTE_INDEX_NOT_CONFIGURED.get(index); |
| | | throw new JebException(msg); |
| | | } |
| | | AttributeIndex attrIndex = |
| | | entryContainer.getAttributeIndex(attrType); |
| | | if (attrIndex == null) |
| | | { |
| | | LocalizableMessage msg = ERR_JEB_ATTRIBUTE_INDEX_NOT_CONFIGURED.get(index); |
| | | throw new JebException(msg); |
| | | } |
| | | attrIndexList.add(attrIndex); |
| | | } |
| | | } |
| | | } |
| | | |
| | | entryLimitMap = |
| | | new IdentityHashMap<Index,HashMap<ByteString,Long>>( |
| | | attrIndexList.size()); |
| | | |
| | | // We will be updating these files independently of the indexes |
| | | // so we need direct access to them rather than going through |
| | | // the entry entryContainer methods. |
| | | id2entry = entryContainer.getID2Entry(); |
| | | dn2id = entryContainer.getDN2ID(); |
| | | id2c = entryContainer.getID2Children(); |
| | | id2s = entryContainer.getID2Subtree(); |
| | | |
| | | // Make a note of the time we started. |
| | | long startTime = System.currentTimeMillis(); |
| | | |
| | | // Start a timer for the progress report. |
| | | Timer timer = new Timer(); |
| | | TimerTask progressTask = new ProgressTask(); |
| | | if (cleanMode) |
| | | { |
| | | // Create a new progressTask based on the index count. |
| | | progressTask = new ProgressTask(true); |
| | | } |
| | | timer.scheduleAtFixedRate(progressTask, progressInterval, |
| | | progressInterval); |
| | | |
| | | // Iterate through the index keys. |
| | | try |
| | | { |
| | | if (cleanMode) |
| | | { |
| | | iterateIndex(txn); |
| | | } |
| | | else |
| | | { |
| | | iterateID2Entry(txn); |
| | | |
| | | // Make sure the vlv indexes are in correct order. |
| | | for(VLVIndex vlvIndex : vlvIndexList) |
| | | { |
| | | iterateVLVIndex(txn, vlvIndex, false); |
| | | } |
| | | } |
| | | } |
| | | finally |
| | | { |
| | | timer.cancel(); |
| | | } |
| | | |
| | | long finishTime = System.currentTimeMillis(); |
| | | long totalTime = finishTime - startTime; |
| | | |
| | | float rate = 0; |
| | | if (totalTime > 0) |
| | | { |
| | | rate = 1000f*keyCount / totalTime; |
| | | } |
| | | |
| | | addStatEntry(statEntry, "verify-error-count", String.valueOf(errorCount)); |
| | | addStatEntry(statEntry, "verify-key-count", String.valueOf(keyCount)); |
| | | if (cleanMode) |
| | | { |
| | | logger.info(NOTE_JEB_VERIFY_CLEAN_FINAL_STATUS, keyCount, errorCount, totalTime/1000, rate); |
| | | |
| | | if (multiReferenceCount > 0) |
| | | { |
| | | float averageEntryReferences = 0; |
| | | if (keyCount > 0) |
| | | { |
| | | averageEntryReferences = entryReferencesCount/keyCount; |
| | | } |
| | | |
| | | logger.debug(INFO_JEB_VERIFY_MULTIPLE_REFERENCE_COUNT, multiReferenceCount); |
| | | addStatEntry(statEntry, "verify-multiple-reference-count", |
| | | String.valueOf(multiReferenceCount)); |
| | | |
| | | logger.debug(INFO_JEB_VERIFY_ENTRY_LIMIT_EXCEEDED_COUNT, entryLimitExceededCount); |
| | | addStatEntry(statEntry, "verify-entry-limit-exceeded-count", |
| | | String.valueOf(entryLimitExceededCount)); |
| | | |
| | | logger.debug(INFO_JEB_VERIFY_AVERAGE_REFERENCE_COUNT, averageEntryReferences); |
| | | addStatEntry(statEntry, "verify-average-reference-count", |
| | | String.valueOf(averageEntryReferences)); |
| | | |
| | | logger.debug(INFO_JEB_VERIFY_MAX_REFERENCE_COUNT, maxEntryPerValue); |
| | | addStatEntry(statEntry, "verify-max-reference-count", |
| | | String.valueOf(maxEntryPerValue)); |
| | | } |
| | | } |
| | | else |
| | | { |
| | | logger.info(NOTE_JEB_VERIFY_FINAL_STATUS, keyCount, errorCount, totalTime/1000, rate); |
| | | //TODO add entry-limit-stats to the statEntry |
| | | if (entryLimitMap.size() > 0) |
| | | { |
| | | logger.debug(INFO_JEB_VERIFY_ENTRY_LIMIT_STATS_HEADER); |
| | | |
| | | for (Map.Entry<Index,HashMap<ByteString,Long>> mapEntry : |
| | | entryLimitMap.entrySet()) |
| | | { |
| | | Index index = mapEntry.getKey(); |
| | | Long[] values = mapEntry.getValue().values().toArray(new Long[0]); |
| | | |
| | | // Calculate the median value for entry limit exceeded. |
| | | Arrays.sort(values); |
| | | long medianValue; |
| | | int x = values.length / 2; |
| | | if (values.length % 2 == 0) |
| | | { |
| | | medianValue = (values[x] + values[x-1]) / 2; |
| | | } |
| | | else |
| | | { |
| | | medianValue = values[x]; |
| | | } |
| | | |
| | | logger.debug(INFO_JEB_VERIFY_ENTRY_LIMIT_STATS_ROW, index, values.length, values[0], |
| | | values[values.length-1], medianValue); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | finally |
| | | { |
| | | entryContainer.sharedLock.unlock(); |
| | | } |
| | | return errorCount; |
| | | } |
| | | |
| | | /** |
| | | * Iterate through the entries in id2entry to perform a check for |
| | | * index completeness. We check that the ID for the entry is indeed |
| | | * present in the indexes for the appropriate values. |
| | | * |
| | | * @throws StorageRuntimeException If an error occurs in the JE database. |
| | | */ |
| | | private void iterateID2Entry(ReadableStorage txn) throws StorageRuntimeException |
| | | { |
| | | Cursor cursor = id2entry.openCursor(txn); |
| | | try |
| | | { |
| | | long storedEntryCount = id2entry.getRecordCount(); |
| | | while (cursor.next()) |
| | | { |
| | | ByteString key = cursor.getKey(); |
| | | ByteString value = cursor.getValue(); |
| | | |
| | | EntryID entryID; |
| | | try |
| | | { |
| | | entryID = new EntryID(key); |
| | | } |
| | | catch (Exception e) |
| | | { |
| | | errorCount++; |
| | | if (logger.isTraceEnabled()) |
| | | { |
| | | logger.traceException(e); |
| | | |
| | | logger.trace("Malformed id2entry ID %s.%n", StaticUtils.bytesToHex(key)); |
| | | } |
| | | continue; |
| | | } |
| | | |
| | | keyCount++; |
| | | |
| | | Entry entry; |
| | | try |
| | | { |
| | | entry = ID2Entry.entryFromDatabase(value, rootContainer.getCompressedSchema()); |
| | | } |
| | | catch (Exception e) |
| | | { |
| | | errorCount++; |
| | | if (logger.isTraceEnabled()) |
| | | { |
| | | logger.traceException(e); |
| | | |
| | | logger.trace("Malformed id2entry record for ID %d:%n%s%n", entryID, StaticUtils.bytesToHex(value)); |
| | | } |
| | | continue; |
| | | } |
| | | |
| | | verifyEntry(txn, entryID, entry); |
| | | } |
| | | if (keyCount != storedEntryCount) |
| | | { |
| | | errorCount++; |
| | | if (logger.isTraceEnabled()) |
| | | { |
| | | logger.trace("The stored entry count in id2entry (%d) does " + |
| | | "not agree with the actual number of entry " + |
| | | "records found (%d).%n", storedEntryCount, keyCount); |
| | | } |
| | | } |
| | | } |
| | | finally |
| | | { |
| | | cursor.close(); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Iterate through the entries in an index to perform a check for |
| | | * index cleanliness. For each ID in the index we check that the |
| | | * entry it refers to does indeed contain the expected value. |
| | | * |
| | | * @throws JebException If an error occurs in the JE backend. |
| | | * @throws StorageRuntimeException If an error occurs in the JE database. |
| | | * @throws DirectoryException If an error occurs reading values in the index. |
| | | */ |
| | | private void iterateIndex(ReadableStorage txn) |
| | | throws JebException, StorageRuntimeException, DirectoryException |
| | | { |
| | | if (verifyDN2ID) |
| | | { |
| | | iterateDN2ID(txn); |
| | | } |
| | | else if (verifyID2Children) |
| | | { |
| | | iterateID2Children(txn); |
| | | } |
| | | else if (verifyID2Subtree) |
| | | { |
| | | iterateID2Subtree(txn); |
| | | } |
| | | else if (attrIndexList.size() > 0) |
| | | { |
| | | AttributeIndex attrIndex = attrIndexList.get(0); |
| | | final IndexingOptions options = attrIndex.getIndexingOptions(); |
| | | iterateAttrIndex(txn, attrIndex.getEqualityIndex(), options); |
| | | iterateAttrIndex(txn, attrIndex.getPresenceIndex(), options); |
| | | iterateAttrIndex(txn, attrIndex.getSubstringIndex(), options); |
| | | iterateAttrIndex(txn, attrIndex.getOrderingIndex(), options); |
| | | iterateAttrIndex(txn, attrIndex.getApproximateIndex(), options); |
| | | // TODO: Need to iterate through ExtendedMatchingRules indexes. |
| | | } |
| | | else if (vlvIndexList.size() > 0) |
| | | { |
| | | iterateVLVIndex(txn, vlvIndexList.get(0), true); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Iterate through the entries in DN2ID to perform a check for |
| | | * index cleanliness. |
| | | * |
| | | * @throws StorageRuntimeException If an error occurs in the JE database. |
| | | */ |
| | | private void iterateDN2ID(ReadableStorage txn) throws StorageRuntimeException |
| | | { |
| | | Cursor cursor = dn2id.openCursor(txn); |
| | | try |
| | | { |
| | | while (cursor.next()) |
| | | { |
| | | keyCount++; |
| | | |
| | | ByteString key = cursor.getKey(); |
| | | ByteString value = cursor.getValue(); |
| | | |
| | | EntryID entryID; |
| | | try |
| | | { |
| | | entryID = new EntryID(value); |
| | | } |
| | | catch (Exception e) |
| | | { |
| | | errorCount++; |
| | | if (logger.isTraceEnabled()) |
| | | { |
| | | logger.traceException(e); |
| | | |
| | | logger.trace("File dn2id has malformed ID for DN <%s>:%n%s%n", key, StaticUtils.bytesToHex(value)); |
| | | } |
| | | continue; |
| | | } |
| | | |
| | | Entry entry; |
| | | try |
| | | { |
| | | entry = id2entry.get(txn, entryID, false); |
| | | } |
| | | catch (Exception e) |
| | | { |
| | | errorCount++; |
| | | logger.traceException(e); |
| | | continue; |
| | | } |
| | | |
| | | if (entry == null) |
| | | { |
| | | errorCount++; |
| | | if (logger.isTraceEnabled()) |
| | | { |
| | | logger.trace("File dn2id has DN <%s> referencing unknown ID %d%n", key, entryID); |
| | | } |
| | | } |
| | | else if (!key.equals(dnToDNKey(entry.getName(), verifyConfig.getBaseDN().size()))) |
| | | { |
| | | errorCount++; |
| | | if (logger.isTraceEnabled()) |
| | | { |
| | | logger.trace("File dn2id has DN <%s> referencing entry with wrong DN <%s>%n", key, entry.getName()); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | finally |
| | | { |
| | | cursor.close(); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Iterate through the entries in ID2Children to perform a check for |
| | | * index cleanliness. |
| | | * |
| | | * @throws JebException If an error occurs in the JE backend. |
| | | * @throws StorageRuntimeException If an error occurs in the JE database. |
| | | */ |
| | | private void iterateID2Children(ReadableStorage txn) throws JebException, StorageRuntimeException |
| | | { |
| | | Cursor cursor = id2c.openCursor(txn); |
| | | try |
| | | { |
| | | while (cursor.next()) |
| | | { |
| | | keyCount++; |
| | | |
| | | ByteString key = cursor.getKey(); |
| | | ByteString value = cursor.getValue(); |
| | | |
| | | EntryID entryID; |
| | | try |
| | | { |
| | | entryID = new EntryID(key); |
| | | } |
| | | catch (Exception e) |
| | | { |
| | | errorCount++; |
| | | if (logger.isTraceEnabled()) |
| | | { |
| | | logger.traceException(e); |
| | | |
| | | logger.trace("File id2children has malformed ID %s%n", StaticUtils.bytesToHex(key)); |
| | | } |
| | | continue; |
| | | } |
| | | |
| | | EntryIDSet entryIDList; |
| | | |
| | | try |
| | | { |
| | | JebFormat.entryIDListFromDatabase(value); |
| | | entryIDList = new EntryIDSet(key, value); |
| | | } |
| | | catch (Exception e) |
| | | { |
| | | errorCount++; |
| | | if (logger.isTraceEnabled()) |
| | | { |
| | | logger.traceException(e); |
| | | |
| | | logger.trace("File id2children has malformed ID list for ID %s:%n%s%n", |
| | | entryID, StaticUtils.bytesToHex(value)); |
| | | } |
| | | continue; |
| | | } |
| | | |
| | | updateIndexStats(entryIDList); |
| | | |
| | | if (entryIDList.isDefined()) |
| | | { |
| | | Entry entry; |
| | | try |
| | | { |
| | | entry = id2entry.get(txn, entryID, false); |
| | | } |
| | | catch (Exception e) |
| | | { |
| | | logger.traceException(e); |
| | | errorCount++; |
| | | continue; |
| | | } |
| | | |
| | | if (entry == null) |
| | | { |
| | | errorCount++; |
| | | if (logger.isTraceEnabled()) |
| | | { |
| | | logger.trace("File id2children has unknown ID %d%n", entryID); |
| | | } |
| | | continue; |
| | | } |
| | | |
| | | for (EntryID id : entryIDList) |
| | | { |
| | | Entry childEntry; |
| | | try |
| | | { |
| | | childEntry = id2entry.get(txn, id, false); |
| | | } |
| | | catch (Exception e) |
| | | { |
| | | logger.traceException(e); |
| | | errorCount++; |
| | | continue; |
| | | } |
| | | |
| | | if (childEntry == null) |
| | | { |
| | | errorCount++; |
| | | if (logger.isTraceEnabled()) |
| | | { |
| | | logger.trace("File id2children has ID %d referencing " + |
| | | "unknown ID %d%n", entryID, id); |
| | | } |
| | | continue; |
| | | } |
| | | |
| | | if (!childEntry.getName().isDescendantOf(entry.getName()) || |
| | | childEntry.getName().size() != |
| | | entry.getName().size() + 1) |
| | | { |
| | | errorCount++; |
| | | if (logger.isTraceEnabled()) |
| | | { |
| | | logger.trace("File id2children has ID %d with DN <%s> " + |
| | | "referencing ID %d with non-child DN <%s>%n", |
| | | entryID, entry.getName(), id, childEntry.getName()); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | finally |
| | | { |
| | | cursor.close(); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Iterate through the entries in ID2Subtree to perform a check for |
| | | * index cleanliness. |
| | | * |
| | | * @throws JebException If an error occurs in the JE backend. |
| | | * @throws StorageRuntimeException If an error occurs in the JE database. |
| | | */ |
| | | private void iterateID2Subtree(ReadableStorage txn) throws JebException, StorageRuntimeException |
| | | { |
| | | Cursor cursor = id2s.openCursor(txn); |
| | | try |
| | | { |
| | | while (cursor.next()) |
| | | { |
| | | keyCount++; |
| | | |
| | | ByteString key = cursor.getKey(); |
| | | ByteString value = cursor.getValue(); |
| | | |
| | | EntryID entryID; |
| | | try |
| | | { |
| | | entryID = new EntryID(key); |
| | | } |
| | | catch (Exception e) |
| | | { |
| | | errorCount++; |
| | | if (logger.isTraceEnabled()) |
| | | { |
| | | logger.traceException(e); |
| | | |
| | | logger.trace("File id2subtree has malformed ID %s%n", StaticUtils.bytesToHex(key)); |
| | | } |
| | | continue; |
| | | } |
| | | |
| | | EntryIDSet entryIDList; |
| | | try |
| | | { |
| | | JebFormat.entryIDListFromDatabase(value); |
| | | entryIDList = new EntryIDSet(key, value); |
| | | } |
| | | catch (Exception e) |
| | | { |
| | | errorCount++; |
| | | if (logger.isTraceEnabled()) |
| | | { |
| | | logger.traceException(e); |
| | | |
| | | logger.trace("File id2subtree has malformed ID list " + |
| | | "for ID %s:%n%s%n", entryID, |
| | | StaticUtils |
| | | .bytesToHex(value)); |
| | | } |
| | | continue; |
| | | } |
| | | |
| | | updateIndexStats(entryIDList); |
| | | |
| | | if (entryIDList.isDefined()) |
| | | { |
| | | Entry entry; |
| | | try |
| | | { |
| | | entry = id2entry.get(txn, entryID, false); |
| | | } |
| | | catch (Exception e) |
| | | { |
| | | logger.traceException(e); |
| | | errorCount++; |
| | | continue; |
| | | } |
| | | |
| | | if (entry == null) |
| | | { |
| | | errorCount++; |
| | | if (logger.isTraceEnabled()) |
| | | { |
| | | logger.trace("File id2subtree has unknown ID %d%n", entryID); |
| | | } |
| | | continue; |
| | | } |
| | | |
| | | for (EntryID id : entryIDList) |
| | | { |
| | | Entry subordEntry; |
| | | try |
| | | { |
| | | subordEntry = id2entry.get(txn, id, false); |
| | | } |
| | | catch (Exception e) |
| | | { |
| | | logger.traceException(e); |
| | | errorCount++; |
| | | continue; |
| | | } |
| | | |
| | | if (subordEntry == null) |
| | | { |
| | | errorCount++; |
| | | if (logger.isTraceEnabled()) |
| | | { |
| | | logger.trace("File id2subtree has ID %d referencing " + |
| | | "unknown ID %d%n", entryID, id); |
| | | } |
| | | continue; |
| | | } |
| | | |
| | | if (!subordEntry.getName().isDescendantOf(entry.getName())) |
| | | { |
| | | errorCount++; |
| | | if (logger.isTraceEnabled()) |
| | | { |
| | | logger.trace("File id2subtree has ID %d with DN <%s> " + |
| | | "referencing ID %d with non-subordinate DN <%s>%n", |
| | | entryID, entry.getName(), id, subordEntry |
| | | .getName()); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | finally |
| | | { |
| | | cursor.close(); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Increment the counter for a key that has exceeded the |
| | | * entry limit. The counter gives the number of entries that have |
| | | * referenced the key. |
| | | * |
| | | * @param index The index containing the key. |
| | | * @param key A key that has exceeded the entry limit. |
| | | */ |
| | | private void incrEntryLimitStats(Index index, ByteString key) |
| | | { |
| | | HashMap<ByteString,Long> hashMap = entryLimitMap.get(index); |
| | | if (hashMap == null) |
| | | { |
| | | hashMap = new HashMap<ByteString, Long>(); |
| | | entryLimitMap.put(index, hashMap); |
| | | } |
| | | Long counter = hashMap.get(key); |
| | | if (counter != null) |
| | | { |
| | | counter++; |
| | | } |
| | | else |
| | | { |
| | | counter = 1L; |
| | | } |
| | | hashMap.put(key, counter); |
| | | } |
| | | |
| | | /** |
| | | * Update the statistical information for an index record. |
| | | * |
| | | * @param entryIDSet The set of entry IDs for the index record. |
| | | */ |
| | | private void updateIndexStats(EntryIDSet entryIDSet) |
| | | { |
| | | if (!entryIDSet.isDefined()) |
| | | { |
| | | entryLimitExceededCount++; |
| | | multiReferenceCount++; |
| | | } |
| | | else |
| | | { |
| | | if (entryIDSet.size() > 1) |
| | | { |
| | | multiReferenceCount++; |
| | | } |
| | | entryReferencesCount += entryIDSet.size(); |
| | | maxEntryPerValue = Math.max(maxEntryPerValue, entryIDSet.size()); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Iterate through the entries in a VLV index to perform a check for index |
| | | * cleanliness. |
| | | * |
| | | * @param vlvIndex The VLV index to perform the check against. |
| | | * @param verifyID True to verify the IDs against id2entry. |
| | | * @throws JebException If an error occurs in the JE backend. |
| | | * @throws StorageRuntimeException If an error occurs in the JE database. |
| | | * @throws DirectoryException If an error occurs reading values in the index. |
| | | */ |
| | | private void iterateVLVIndex(ReadableStorage txn, VLVIndex vlvIndex, boolean verifyID) |
| | | throws JebException, StorageRuntimeException, DirectoryException |
| | | { |
| | | if(vlvIndex == null) |
| | | { |
| | | return; |
| | | } |
| | | |
| | | Cursor cursor = vlvIndex.openCursor(txn); |
| | | try |
| | | { |
| | | SortValues lastValues = null; |
| | | while (cursor.next()) |
| | | { |
| | | ByteString key = cursor.getKey(); |
| | | ByteString value = cursor.getValue(); |
| | | |
| | | SortValuesSet sortValuesSet = new SortValuesSet(key, value, vlvIndex); |
| | | for(int i = 0; i < sortValuesSet.getEntryIDs().length; i++) |
| | | { |
| | | keyCount++; |
| | | SortValues values = sortValuesSet.getSortValues(i); |
| | | if(lastValues != null && lastValues.compareTo(values) >= 1) |
| | | { |
| | | // Make sure the values is larger then the previous one. |
| | | if(logger.isTraceEnabled()) |
| | | { |
| | | logger.trace("Values %s and %s are incorrectly ordered", |
| | | lastValues, values, keyDump(vlvIndex, |
| | | sortValuesSet.getKeySortValues())); |
| | | } |
| | | errorCount++; |
| | | } |
| | | if (i == sortValuesSet.getEntryIDs().length - 1 && key.length() != 0) |
| | | { |
| | | // If this is the last one in a bounded set, make sure it is the |
| | | // same as the database key. |
| | | ByteString encodedKey = vlvIndex.encodeKey( |
| | | values.getEntryID(), values.getValues(), values.getTypes()); |
| | | if (!key.equals(encodedKey)) |
| | | { |
| | | if(logger.isTraceEnabled()) |
| | | { |
| | | logger.trace("Incorrect key for SortValuesSet in VLV " + |
| | | "index %s. Last values bytes %s, Key bytes %s", |
| | | vlvIndex.getName(), encodedKey, key); |
| | | } |
| | | errorCount++; |
| | | } |
| | | } |
| | | lastValues = values; |
| | | |
| | | if(verifyID) |
| | | { |
| | | Entry entry; |
| | | EntryID id = new EntryID(values.getEntryID()); |
| | | try |
| | | { |
| | | entry = id2entry.get(txn, id, false); |
| | | } |
| | | catch (Exception e) |
| | | { |
| | | logger.traceException(e); |
| | | errorCount++; |
| | | continue; |
| | | } |
| | | |
| | | if (entry == null) |
| | | { |
| | | errorCount++; |
| | | if (logger.isTraceEnabled()) |
| | | { |
| | | logger.trace("Reference to unknown ID %d%n%s", |
| | | id, keyDump(vlvIndex, sortValuesSet.getKeySortValues())); |
| | | } |
| | | continue; |
| | | } |
| | | |
| | | SortValues entryValues = new SortValues(id, entry, vlvIndex.sortOrder); |
| | | if(entryValues.compareTo(values) != 0) |
| | | { |
| | | errorCount++; |
| | | if(logger.isTraceEnabled()) |
| | | { |
| | | logger.trace("Reference to entry ID %d " + |
| | | "which does not match the values%n%s", |
| | | id, keyDump(vlvIndex, sortValuesSet.getKeySortValues())); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | finally |
| | | { |
| | | cursor.close(); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Iterate through the entries in an attribute index to perform a check for |
| | | * index cleanliness. |
| | | * @param index The index database to be checked. |
| | | * @throws JebException If an error occurs in the JE backend. |
| | | * @throws StorageRuntimeException If an error occurs in the JE database. |
| | | */ |
| | | private void iterateAttrIndex(ReadableStorage txn, Index index, IndexingOptions options) |
| | | throws JebException, StorageRuntimeException |
| | | { |
| | | if (index == null) |
| | | { |
| | | return; |
| | | } |
| | | |
| | | Cursor cursor = index.openCursor(txn); |
| | | try |
| | | { |
| | | while (cursor.next()) |
| | | { |
| | | keyCount++; |
| | | |
| | | final ByteString key = cursor.getKey(); |
| | | ByteString value = cursor.getValue(); |
| | | |
| | | EntryIDSet entryIDList; |
| | | try |
| | | { |
| | | JebFormat.entryIDListFromDatabase(value); |
| | | entryIDList = new EntryIDSet(key, value); |
| | | } |
| | | catch (Exception e) |
| | | { |
| | | errorCount++; |
| | | if (logger.isTraceEnabled()) |
| | | { |
| | | logger.traceException(e); |
| | | |
| | | logger.trace("Malformed ID list: %s%n%s", |
| | | StaticUtils.bytesToHex(value), keyDump(index, key)); |
| | | } |
| | | continue; |
| | | } |
| | | |
| | | updateIndexStats(entryIDList); |
| | | |
| | | if (entryIDList.isDefined()) |
| | | { |
| | | EntryID prevID = null; |
| | | |
| | | for (EntryID id : entryIDList) |
| | | { |
| | | if (prevID != null && id.equals(prevID) && logger.isTraceEnabled()) |
| | | { |
| | | logger.trace("Duplicate reference to ID %d%n%s", id, keyDump(index, key)); |
| | | } |
| | | prevID = id; |
| | | |
| | | Entry entry; |
| | | try |
| | | { |
| | | entry = id2entry.get(txn, id, false); |
| | | } |
| | | catch (Exception e) |
| | | { |
| | | logger.traceException(e); |
| | | errorCount++; |
| | | continue; |
| | | } |
| | | |
| | | if (entry == null) |
| | | { |
| | | errorCount++; |
| | | if (logger.isTraceEnabled()) |
| | | { |
| | | logger.trace("Reference to unknown ID %d%n%s", id, keyDump(index, key)); |
| | | } |
| | | continue; |
| | | } |
| | | |
| | | // As an optimization avoid passing in a real set and wasting time |
| | | // hashing and comparing a potentially large set of values, as well |
| | | // as using up memory. Instead just intercept the add() method and |
| | | // detect when an equivalent value has been added. |
| | | |
| | | // We need to use an AtomicBoolean here since anonymous classes |
| | | // require referenced external variables to be final. |
| | | final AtomicBoolean foundMatchingKey = new AtomicBoolean(false); |
| | | |
| | | Set<ByteString> dummySet = new AbstractSet<ByteString>() |
| | | { |
| | | @Override |
| | | public Iterator<ByteString> iterator() |
| | | { |
| | | // The set is always empty. |
| | | return Collections.<ByteString> emptySet().iterator(); |
| | | } |
| | | |
| | | @Override |
| | | public int size() |
| | | { |
| | | // The set is always empty. |
| | | return 0; |
| | | } |
| | | |
| | | @Override |
| | | public boolean add(ByteString e) |
| | | { |
| | | if (key.equals(e)) |
| | | { |
| | | // We could terminate processing at this point by throwing an |
| | | // UnsupportedOperationException, but this optimization is |
| | | // already ugly enough. |
| | | foundMatchingKey.set(true); |
| | | } |
| | | return true; |
| | | } |
| | | |
| | | }; |
| | | |
| | | index.indexer.indexEntry(entry, dummySet, options); |
| | | |
| | | if (!foundMatchingKey.get()) |
| | | { |
| | | errorCount++; |
| | | if (logger.isTraceEnabled()) |
| | | { |
| | | logger.trace("Reference to entry " |
| | | + "<%s> which does not match the value%n%s", |
| | | entry.getName(), keyDump(index, key)); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | finally |
| | | { |
| | | cursor.close(); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Check that an index is complete for a given entry. |
| | | * |
| | | * @param entryID The entry ID. |
| | | * @param entry The entry to be checked. |
| | | */ |
| | | private void verifyEntry(ReadableStorage txn, EntryID entryID, Entry entry) |
| | | { |
| | | if (verifyDN2ID) |
| | | { |
| | | verifyDN2ID(txn, entryID, entry); |
| | | } |
| | | if (verifyID2Children) |
| | | { |
| | | verifyID2Children(txn, entryID, entry); |
| | | } |
| | | if (verifyID2Subtree) |
| | | { |
| | | verifyID2Subtree(txn, entryID, entry); |
| | | } |
| | | verifyIndex(txn, entryID, entry); |
| | | } |
| | | |
| | | /** |
| | | * Check that the DN2ID index is complete for a given entry. |
| | | * |
| | | * @param entryID The entry ID. |
| | | * @param entry The entry to be checked. |
| | | */ |
| | | private void verifyDN2ID(ReadableStorage txn, EntryID entryID, Entry entry) |
| | | { |
| | | DN dn = entry.getName(); |
| | | |
| | | // Check the ID is in dn2id with the correct DN. |
| | | try |
| | | { |
| | | EntryID id = dn2id.get(txn, dn, false); |
| | | if (id == null) |
| | | { |
| | | if (logger.isTraceEnabled()) |
| | | { |
| | | logger.trace("File dn2id is missing key %s.%n", dn); |
| | | } |
| | | errorCount++; |
| | | } |
| | | else if (!id.equals(entryID)) |
| | | { |
| | | if (logger.isTraceEnabled()) |
| | | { |
| | | logger.trace("File dn2id has ID %d instead of %d for key %s.%n", id, entryID, dn); |
| | | } |
| | | errorCount++; |
| | | } |
| | | } |
| | | catch (Exception e) |
| | | { |
| | | if (logger.isTraceEnabled()) |
| | | { |
| | | logger.traceException(e); |
| | | logger.trace("File dn2id has error reading key %s: %s.%n", dn, e.getMessage()); |
| | | } |
| | | errorCount++; |
| | | } |
| | | |
| | | // Check the parent DN is in dn2id. |
| | | DN parentDN = getParent(dn); |
| | | if (parentDN != null) |
| | | { |
| | | try |
| | | { |
| | | EntryID id = dn2id.get(txn, parentDN, false); |
| | | if (id == null) |
| | | { |
| | | if (logger.isTraceEnabled()) |
| | | { |
| | | logger.trace("File dn2id is missing key %s.%n", parentDN); |
| | | } |
| | | errorCount++; |
| | | } |
| | | } |
| | | catch (Exception e) |
| | | { |
| | | if (logger.isTraceEnabled()) |
| | | { |
| | | logger.traceException(e); |
| | | logger.trace("File dn2id has error reading key %s: %s.%n", parentDN, e.getMessage()); |
| | | } |
| | | errorCount++; |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Check that the ID2Children index is complete for a given entry. |
| | | * |
| | | * @param entryID The entry ID. |
| | | * @param entry The entry to be checked. |
| | | */ |
| | | private void verifyID2Children(ReadableStorage txn, EntryID entryID, Entry entry) |
| | | { |
| | | DN dn = entry.getName(); |
| | | |
| | | DN parentDN = getParent(dn); |
| | | if (parentDN != null) |
| | | { |
| | | EntryID parentID = null; |
| | | try |
| | | { |
| | | parentID = dn2id.get(txn, parentDN, false); |
| | | if (parentID == null) |
| | | { |
| | | if (logger.isTraceEnabled()) |
| | | { |
| | | logger.trace("File dn2id is missing key %s.%n", parentDN); |
| | | } |
| | | errorCount++; |
| | | } |
| | | } |
| | | catch (Exception e) |
| | | { |
| | | if (logger.isTraceEnabled()) |
| | | { |
| | | logger.traceException(e); |
| | | logger.trace("File dn2id has error reading key %s: %s.", parentDN, e.getMessage()); |
| | | } |
| | | errorCount++; |
| | | } |
| | | if (parentID != null) |
| | | { |
| | | try |
| | | { |
| | | ConditionResult cr = id2c.containsID(null, parentID.toByteString(), entryID); |
| | | if (cr == ConditionResult.FALSE) |
| | | { |
| | | if (logger.isTraceEnabled()) |
| | | { |
| | | logger.trace("File id2children is missing ID %d for key %d.%n", entryID, parentID); |
| | | } |
| | | errorCount++; |
| | | } |
| | | else if (cr == ConditionResult.UNDEFINED) |
| | | { |
| | | incrEntryLimitStats(id2c, parentID.toByteString()); |
| | | } |
| | | } |
| | | catch (StorageRuntimeException e) |
| | | { |
| | | if (logger.isTraceEnabled()) |
| | | { |
| | | logger.traceException(e); |
| | | |
| | | logger.trace("File id2children has error reading key %d: %s.", parentID, e.getMessage()); |
| | | } |
| | | errorCount++; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Check that the ID2Subtree index is complete for a given entry. |
| | | * |
| | | * @param entryID The entry ID. |
| | | * @param entry The entry to be checked. |
| | | */ |
| | | private void verifyID2Subtree(ReadableStorage txn, EntryID entryID, Entry entry) |
| | | { |
| | | for (DN dn = getParent(entry.getName()); dn != null; dn = getParent(dn)) |
| | | { |
| | | EntryID id = null; |
| | | try |
| | | { |
| | | id = dn2id.get(txn, dn, false); |
| | | if (id == null) |
| | | { |
| | | if (logger.isTraceEnabled()) |
| | | { |
| | | logger.trace("File dn2id is missing key %s.%n", dn); |
| | | } |
| | | errorCount++; |
| | | } |
| | | } |
| | | catch (Exception e) |
| | | { |
| | | if (logger.isTraceEnabled()) |
| | | { |
| | | logger.traceException(e); |
| | | logger.trace("File dn2id has error reading key %s: %s.%n", dn, e.getMessage()); |
| | | } |
| | | errorCount++; |
| | | } |
| | | if (id != null) |
| | | { |
| | | try |
| | | { |
| | | ConditionResult cr; |
| | | cr = id2s.containsID(null, id.toByteString(), entryID); |
| | | if (cr == ConditionResult.FALSE) |
| | | { |
| | | if (logger.isTraceEnabled()) |
| | | { |
| | | logger.trace("File id2subtree is missing ID %d for key %d.%n", entryID, id); |
| | | } |
| | | errorCount++; |
| | | } |
| | | else if (cr == ConditionResult.UNDEFINED) |
| | | { |
| | | incrEntryLimitStats(id2s, id.toByteString()); |
| | | } |
| | | } |
| | | catch (StorageRuntimeException e) |
| | | { |
| | | if (logger.isTraceEnabled()) |
| | | { |
| | | logger.traceException(e); |
| | | |
| | | logger.trace("File id2subtree has error reading key %d: %s.%n", id, e.getMessage()); |
| | | } |
| | | errorCount++; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Construct a printable string from a raw key value. |
| | | * |
| | | * @param index |
| | | * The index database containing the key value. |
| | | * @param key |
| | | * The bytes of the key. |
| | | * @return A string that may be logged or printed. |
| | | */ |
| | | private String keyDump(Index index, ByteSequence key) |
| | | { |
| | | StringBuilder buffer = new StringBuilder(128); |
| | | buffer.append("File: "); |
| | | buffer.append(index); |
| | | buffer.append(ServerConstants.EOL); |
| | | buffer.append("Key:"); |
| | | buffer.append(ServerConstants.EOL); |
| | | StaticUtils.byteArrayToHexPlusAscii(buffer, key.toByteArray(), 6); |
| | | return buffer.toString(); |
| | | } |
| | | |
| | | /** |
| | | * Construct a printable string from a raw key value. |
| | | * |
| | | * @param vlvIndex The vlvIndex database containing the key value. |
| | | * @param keySortValues THe sort values that is being used as the key. |
| | | * @return A string that may be logged or printed. |
| | | */ |
| | | private String keyDump(VLVIndex vlvIndex, SortValues keySortValues) |
| | | { |
| | | StringBuilder buffer = new StringBuilder(128); |
| | | buffer.append("File: "); |
| | | buffer.append(vlvIndex); |
| | | buffer.append(ServerConstants.EOL); |
| | | buffer.append("Key (last sort values):"); |
| | | if(keySortValues != null) |
| | | { |
| | | buffer.append(keySortValues); |
| | | } |
| | | else |
| | | { |
| | | buffer.append("UNBOUNDED (0x00)"); |
| | | } |
| | | return buffer.toString(); |
| | | } |
| | | |
| | | /** |
| | | * Check that an attribute index is complete for a given entry. |
| | | * |
| | | * @param entryID The entry ID. |
| | | * @param entry The entry to be checked. |
| | | */ |
| | | private void verifyIndex(ReadableStorage txn, EntryID entryID, Entry entry) |
| | | { |
| | | for (AttributeIndex attrIndex : attrIndexList) |
| | | { |
| | | try |
| | | { |
| | | List<Attribute> attrList = |
| | | entry.getAttribute(attrIndex.getAttributeType()); |
| | | if (attrList != null) |
| | | { |
| | | verifyAttribute(attrIndex, entryID, attrList); |
| | | } |
| | | } |
| | | catch (DirectoryException e) |
| | | { |
| | | if (logger.isTraceEnabled()) |
| | | { |
| | | logger.traceException(e); |
| | | |
| | | logger.trace("Error normalizing values of attribute %s in " + |
| | | "entry <%s>: %s.%n", |
| | | attrIndex.getAttributeType(), entry.getName(), e.getMessageObject()); |
| | | } |
| | | } |
| | | } |
| | | |
| | | for (VLVIndex vlvIndex : vlvIndexList) |
| | | { |
| | | try |
| | | { |
| | | if (vlvIndex.shouldInclude(entry) |
| | | && !vlvIndex.containsValues(null, entryID.longValue(), |
| | | vlvIndex.getSortValues(entry), vlvIndex.getSortTypes())) |
| | | { |
| | | if(logger.isTraceEnabled()) |
| | | { |
| | | logger.trace("Missing entry %s in VLV index %s", entry.getName(), vlvIndex.getName()); |
| | | } |
| | | errorCount++; |
| | | } |
| | | } |
| | | catch (DirectoryException e) |
| | | { |
| | | if (logger.isTraceEnabled()) |
| | | { |
| | | logger.traceException(e); |
| | | logger.trace("Error checking entry %s against filter or base DN for VLV index %s: %s", |
| | | entry.getName(), vlvIndex.getName(), e.getMessageObject()); |
| | | } |
| | | errorCount++; |
| | | } |
| | | catch (StorageRuntimeException e) |
| | | { |
| | | if (logger.isTraceEnabled()) |
| | | { |
| | | logger.traceException(e); |
| | | logger.trace("Error reading VLV index %s for entry %s: %s", |
| | | vlvIndex.getName(), entry.getName(), StaticUtils.getBacktrace(e)); |
| | | } |
| | | errorCount++; |
| | | } |
| | | catch (JebException e) |
| | | { |
| | | if (logger.isTraceEnabled()) |
| | | { |
| | | logger.traceException(e); |
| | | logger.trace("Error reading VLV index %s for entry %s: %s", |
| | | vlvIndex.getName(), entry.getName(), StaticUtils.getBacktrace(e)); |
| | | } |
| | | errorCount++; |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Check that an attribute index is complete for a given attribute. |
| | | * |
| | | * @param attrIndex The attribute index to be checked. |
| | | * @param entryID The entry ID. |
| | | * @param attrList The attribute to be checked. |
| | | * @throws DirectoryException If a Directory Server error occurs. |
| | | */ |
| | | private void verifyAttribute(AttributeIndex attrIndex, EntryID entryID, |
| | | List<Attribute> attrList) |
| | | throws DirectoryException |
| | | { |
| | | if (attrList == null || attrList.isEmpty()) |
| | | { |
| | | return; |
| | | } |
| | | |
| | | ReadableStorage txn = null; // FIXME JNR |
| | | Index equalityIndex = attrIndex.getEqualityIndex(); |
| | | Index presenceIndex = attrIndex.getPresenceIndex(); |
| | | Index substringIndex = attrIndex.getSubstringIndex(); |
| | | Index orderingIndex = attrIndex.getOrderingIndex(); |
| | | Index approximateIndex = attrIndex.getApproximateIndex(); |
| | | // TODO: Add support for Extended Matching Rules indexes. |
| | | |
| | | if (presenceIndex != null) |
| | | { |
| | | verifyAttributeInIndex(presenceIndex, txn, PresenceIndexer.presenceKey, entryID); |
| | | } |
| | | |
| | | for (Attribute attr : attrList) |
| | | { |
| | | final AttributeType attrType = attr.getAttributeType(); |
| | | MatchingRule equalityRule = attrType.getEqualityMatchingRule(); |
| | | for (ByteString value : attr) |
| | | { |
| | | ByteString normalizedBytes = normalize(equalityRule, value); |
| | | |
| | | if (equalityIndex != null) |
| | | { |
| | | verifyAttributeInIndex(equalityIndex, txn, normalizedBytes, entryID); |
| | | } |
| | | |
| | | if (substringIndex != null) |
| | | { |
| | | for (ByteString key : attrIndex.substringKeys(normalizedBytes)) |
| | | { |
| | | verifyAttributeInIndex(substringIndex, txn, key, entryID); |
| | | } |
| | | } |
| | | |
| | | if (orderingIndex != null) |
| | | { |
| | | ByteString key = normalize(attrType.getOrderingMatchingRule(), value); |
| | | verifyAttributeInIndex(orderingIndex, txn, key, entryID); |
| | | } |
| | | |
| | | if (approximateIndex != null) |
| | | { |
| | | ByteString key = normalize(attrType.getApproximateMatchingRule(), value); |
| | | verifyAttributeInIndex(approximateIndex, txn, key, entryID); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | private void verifyAttributeInIndex(Index index, ReadableStorage txn, |
| | | ByteString key, EntryID entryID) |
| | | { |
| | | try |
| | | { |
| | | ConditionResult cr = index.containsID(txn, key, entryID); |
| | | if (cr == ConditionResult.FALSE) |
| | | { |
| | | if (logger.isTraceEnabled()) |
| | | { |
| | | logger.trace("Missing ID %d%n%s", entryID, keyDump(index, key)); |
| | | } |
| | | errorCount++; |
| | | } |
| | | else if (cr == ConditionResult.UNDEFINED) |
| | | { |
| | | incrEntryLimitStats(index, key); |
| | | } |
| | | } |
| | | catch (StorageRuntimeException e) |
| | | { |
| | | if (logger.isTraceEnabled()) |
| | | { |
| | | logger.traceException(e); |
| | | |
| | | logger.trace("Error reading database: %s%n%s", e.getMessage(), keyDump(index, key)); |
| | | } |
| | | errorCount++; |
| | | } |
| | | } |
| | | |
| | | private ByteString normalize(MatchingRule matchingRule, ByteString value) throws DirectoryException |
| | | { |
| | | try |
| | | { |
| | | return matchingRule.normalizeAttributeValue(value); |
| | | } |
| | | catch (DecodeException e) |
| | | { |
| | | throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX, |
| | | e.getMessageObject(), e); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Get the parent DN of a given DN. |
| | | * |
| | | * @param dn The DN. |
| | | * @return The parent DN or null if the given DN is a base DN. |
| | | */ |
| | | private DN getParent(DN dn) |
| | | { |
| | | if (dn.equals(verifyConfig.getBaseDN())) |
| | | { |
| | | return null; |
| | | } |
| | | return dn.getParentDNInSuffix(); |
| | | } |
| | | |
| | | /** |
| | | * This class reports progress of the verify job at fixed intervals. |
| | | */ |
| | | private class ProgressTask extends TimerTask |
| | | { |
| | | /** |
| | | * The total number of records to process. |
| | | */ |
| | | private long totalCount; |
| | | |
| | | /** |
| | | * The number of records that had been processed at the time of the |
| | | * previous progress report. |
| | | */ |
| | | private long previousCount; |
| | | |
| | | /** |
| | | * The time in milliseconds of the previous progress report. |
| | | */ |
| | | private long previousTime; |
| | | |
| | | /** |
| | | * The environment statistics at the time of the previous report. |
| | | */ |
| | | private EnvironmentStats prevEnvStats; |
| | | |
| | | /** |
| | | * The number of bytes in a megabyte. |
| | | * Note that 1024*1024 bytes may eventually become known as a mebibyte(MiB). |
| | | */ |
| | | private static final int bytesPerMegabyte = 1024*1024; |
| | | |
| | | /** |
| | | * Create a new verify progress task. |
| | | * @throws StorageRuntimeException An error occurred while accessing the JE |
| | | * database. |
| | | */ |
| | | public ProgressTask() throws StorageRuntimeException |
| | | { |
| | | previousTime = System.currentTimeMillis(); |
| | | prevEnvStats = |
| | | rootContainer.getEnvironmentStats(new StatsConfig()); |
| | | totalCount = rootContainer.getEntryContainer( |
| | | verifyConfig.getBaseDN()).getEntryCount(); |
| | | } |
| | | |
| | | /** |
| | | * Create a new verify progress task. |
| | | * @param indexIterator boolean, indicates if the task is iterating |
| | | * through indexes or the entries. |
| | | * @throws StorageRuntimeException An error occurred while accessing the JE |
| | | * database. |
| | | */ |
| | | private ProgressTask(boolean indexIterator) throws StorageRuntimeException |
| | | { |
| | | previousTime = System.currentTimeMillis(); |
| | | prevEnvStats = rootContainer.getEnvironmentStats(new StatsConfig()); |
| | | |
| | | if (indexIterator) |
| | | { |
| | | if (verifyDN2ID) |
| | | { |
| | | totalCount = dn2id.getRecordCount(); |
| | | } |
| | | else if (verifyID2Children) |
| | | { |
| | | totalCount = id2c.getRecordCount(); |
| | | } |
| | | else if (verifyID2Subtree) |
| | | { |
| | | totalCount = id2s.getRecordCount(); |
| | | } |
| | | else if(attrIndexList.size() > 0) |
| | | { |
| | | AttributeIndex attrIndex = attrIndexList.get(0); |
| | | totalCount = 0; |
| | | if (attrIndex.getEqualityIndex() != null) |
| | | { |
| | | totalCount += attrIndex.getEqualityIndex().getRecordCount(); |
| | | } |
| | | if (attrIndex.getPresenceIndex() != null) |
| | | { |
| | | totalCount += attrIndex.getPresenceIndex().getRecordCount(); |
| | | } |
| | | if (attrIndex.getSubstringIndex() != null) |
| | | { |
| | | totalCount += attrIndex.getSubstringIndex().getRecordCount(); |
| | | } |
| | | if (attrIndex.getOrderingIndex() != null) |
| | | { |
| | | totalCount += attrIndex.getOrderingIndex().getRecordCount(); |
| | | } |
| | | if (attrIndex.getApproximateIndex() != null) |
| | | { |
| | | totalCount += attrIndex.getApproximateIndex().getRecordCount(); |
| | | } |
| | | // TODO: Add support for Extended Matching Rules indexes. |
| | | } |
| | | else if (vlvIndexList.size() > 0) |
| | | { |
| | | totalCount = vlvIndexList.get(0).getRecordCount(); |
| | | } |
| | | } |
| | | else |
| | | { |
| | | totalCount = rootContainer.getEntryContainer( |
| | | verifyConfig.getBaseDN()).getEntryCount(); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * The action to be performed by this timer task. |
| | | */ |
| | | @Override |
| | | public void run() |
| | | { |
| | | long latestCount = keyCount; |
| | | long deltaCount = latestCount - previousCount; |
| | | long latestTime = System.currentTimeMillis(); |
| | | long deltaTime = latestTime - previousTime; |
| | | |
| | | if (deltaTime == 0) |
| | | { |
| | | return; |
| | | } |
| | | |
| | | float rate = 1000f*deltaCount / deltaTime; |
| | | |
| | | logger.info(NOTE_JEB_VERIFY_PROGRESS_REPORT, latestCount, totalCount, errorCount, rate); |
| | | |
| | | try |
| | | { |
| | | Runtime runtime = Runtime.getRuntime(); |
| | | long freeMemory = runtime.freeMemory() / bytesPerMegabyte; |
| | | |
| | | EnvironmentStats envStats = |
| | | rootContainer.getEnvironmentStats(new StatsConfig()); |
| | | long nCacheMiss = |
| | | envStats.getNCacheMiss() - prevEnvStats.getNCacheMiss(); |
| | | |
| | | float cacheMissRate = 0; |
| | | if (deltaCount > 0) |
| | | { |
| | | cacheMissRate = nCacheMiss/(float)deltaCount; |
| | | } |
| | | |
| | | logger.debug(INFO_JEB_VERIFY_CACHE_AND_MEMORY_REPORT, freeMemory, cacheMissRate); |
| | | |
| | | prevEnvStats = envStats; |
| | | } |
| | | catch (StorageRuntimeException e) |
| | | { |
| | | logger.traceException(e); |
| | | } |
| | | |
| | | |
| | | previousCount = latestCount; |
| | | previousTime = latestTime; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Adds an attribute of type t and value v to the statEntry, only if the |
| | | * statEntry is not null. |
| | | * @param statEntry passed in from backentryImpl.verifyBackend. |
| | | * @param t String to be used as the attribute type. |
| | | * @param v String to be used as the attribute value. |
| | | */ |
| | | private void addStatEntry(Entry statEntry, String t, String v) |
| | | { |
| | | if (statEntry != null) |
| | | { |
| | | Attribute a = Attributes.create(t, v); |
| | | statEntry.addAttribute(a, null); |
| | | } |
| | | } |
| | | } |
| opendj3-server-dev/src/server/org/opends/server/util/StaticUtils.java |