/* * 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 profiq s.r.o. * Portions Copyright 2011-2015 ForgeRock AS. */ package org.opends.server.plugins; import static org.opends.messages.PluginMessages.*; import static com.forgerock.opendj.util.StaticUtils.toLowerCase; import java.util.*; import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock; import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock; import org.forgerock.i18n.LocalizableMessage; import org.forgerock.i18n.slf4j.LocalizedLogger; import org.forgerock.opendj.config.server.ConfigChangeResult; 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.AttributeCleanupPluginCfg; import org.opends.server.admin.std.server.PluginCfg; import org.opends.server.api.plugin.DirectoryServerPlugin; import org.opends.server.api.plugin.PluginResult; import org.opends.server.api.plugin.PluginType; import org.opends.server.core.DirectoryServer; import org.opends.server.types.*; import org.opends.server.types.operation.PreParseAddOperation; import org.opends.server.types.operation.PreParseModifyOperation; /** * The attribute cleanup plugin implementation class. The plugin removes and/or * renames the configured parameters from the incoming ADD and MODIFY requests. */ public class AttributeCleanupPlugin extends DirectoryServerPlugin<AttributeCleanupPluginCfg> implements ConfigurationChangeListener<AttributeCleanupPluginCfg> { /** * Plugin configuration. */ private AttributeCleanupPluginCfg config; /** * Debug tracer. */ private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); /** * A table of attributes to be renamed. */ private Map<String, String> attributesToRename; /** * The set of attributes to be removed. */ private Set<String> attributesToRemove; /** * This lock prevents concurrent updates to the configuration while operations * are being processed. */ private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); private final ReadLock sharedLock = lock.readLock(); private final WriteLock exclusiveLock = lock.writeLock(); /** * Default constructor. */ public AttributeCleanupPlugin() { super(); } /** {@inheritDoc} */ @Override public ConfigChangeResult applyConfigurationChange( final AttributeCleanupPluginCfg config) { exclusiveLock.lock(); try { /* Apply the change, as at this point is has been validated. */ this.config = config; attributesToRename = new HashMap<>(); for (final String mapping : config.getRenameInboundAttributes()) { final int colonPos = mapping.lastIndexOf(":"); final String fromAttr = mapping.substring(0, colonPos).trim(); final String toAttr = mapping.substring(colonPos + 1).trim(); attributesToRename.put(toLowerCase(fromAttr), toLowerCase(toAttr)); } attributesToRemove = new HashSet<>(); for (final String attr : config.getRemoveInboundAttributes()) { attributesToRemove.add(toLowerCase(attr.trim())); } /* Update was successful, no restart required. */ return new ConfigChangeResult(); } finally { exclusiveLock.unlock(); } } /** {@inheritDoc} */ @Override public PluginResult.PreParse doPreParse( final PreParseAddOperation addOperation) { sharedLock.lock(); try { /* * First strip the listed attributes, then rename the ones that remain. */ processInboundRemove(addOperation); processInboundRename(addOperation); return PluginResult.PreParse.continueOperationProcessing(); } finally { sharedLock.unlock(); } } /** {@inheritDoc} */ @Override public PluginResult.PreParse doPreParse( final PreParseModifyOperation modifyOperation) { sharedLock.lock(); try { /* * First strip the listed attributes, then rename the ones that remain. */ processInboundRemove(modifyOperation); processInboundRename(modifyOperation); /* * If the MODIFY request has been stripped of ALL modifications, stop the * processing and return SUCCESS to the client. */ if (modifyOperation.getRawModifications().isEmpty()) { if (logger.isTraceEnabled()) { logger.trace("The AttributeCleanupPlugin has eliminated all " + "modifications. The processing should be stopped."); } return PluginResult.PreParse.stopProcessing(ResultCode.SUCCESS, null); } return PluginResult.PreParse.continueOperationProcessing(); } finally { sharedLock.unlock(); } } /** {@inheritDoc} */ @Override public void finalizePlugin() { /* * It's not essential to take the lock here, but we will anyhow for * consistency with other methods. */ exclusiveLock.lock(); try { /* Deregister change listeners. */ config.removeAttributeCleanupChangeListener(this); } finally { exclusiveLock.unlock(); } } /** {@inheritDoc} */ @Override public void initializePlugin(final Set<PluginType> pluginTypes, final AttributeCleanupPluginCfg configuration) throws ConfigException, InitializationException { /* * The plugin should be invoked only for pre-parse ADD and MODIFY * operations. */ for (final PluginType t : pluginTypes) { switch (t) { case PRE_PARSE_ADD: break; case PRE_PARSE_MODIFY: break; default: throw new ConfigException(ERR_PLUGIN_ATTR_CLEANUP_INITIALIZE_PLUGIN.get(t)); } } /* Verify the current configuration. */ final List<LocalizableMessage> messages = new LinkedList<>(); if (!isConfigurationChangeAcceptable(configuration, messages)) { throw new ConfigException(messages.get(0)); } /* Register change listeners. */ configuration.addAttributeCleanupChangeListener(this); /* Save the configuration. */ applyConfigurationChange(configuration); } /** {@inheritDoc} */ @Override public boolean isConfigurationAcceptable(final PluginCfg configuration, final List<LocalizableMessage> unacceptableReasons) { final AttributeCleanupPluginCfg cfg = (AttributeCleanupPluginCfg) configuration; return isConfigurationChangeAcceptable(cfg, unacceptableReasons); } /** {@inheritDoc} */ @Override public boolean isConfigurationChangeAcceptable( final AttributeCleanupPluginCfg config, final List<LocalizableMessage> messages) { /* * The admin framework will ensure that there are no duplicate attributes to * be removed. */ boolean isValid = true; /* * Verify that there are no duplicate mappings and that attributes are * renamed to valid attribute types. */ final Set<String> fromAttrs = new HashSet<>(); for (final String attr : config.getRenameInboundAttributes()) { /* * The format is: from:to where each 'from' and 'to' are attribute * descriptions. The admin framework ensures that the format is correct. */ final int colonPos = attr.lastIndexOf(":"); final String fromAttr = attr.substring(0, colonPos).trim(); final String toAttr = attr.substring(colonPos + 1).trim(); /* * Make sure that toAttr is defined within the server, being careful to * ignore attribute options. */ final int semicolonPos = toAttr.indexOf(";"); final String toAttrType = semicolonPos < 0 && semicolonPos < toAttr.length() - 1 ? toAttr : toAttr.substring(semicolonPos + 1); if (DirectoryServer.getAttributeTypeOrNull(toLowerCase(toAttrType)) == null) { messages.add(ERR_PLUGIN_ATTR_CLEANUP_ATTRIBUTE_MISSING.get(toAttr)); isValid = false; } // Check for duplicates. final String nfromAttr = toLowerCase(fromAttr); if (!fromAttrs.add(nfromAttr)) { messages.add(ERR_PLUGIN_ATTR_CLEANUP_DUPLICATE_VALUE.get(fromAttr)); isValid = false; } // Check that attribute does not map to itself. if (nfromAttr.equals(toLowerCase(toAttr))) { messages.add(ERR_PLUGIN_ATTR_CLEANUP_EQUAL_VALUES.get(fromAttr, toAttr)); isValid = false; } } return isValid; } /** * Remove the attributes listed in the configuration under * ds-cfg-remove-inbound-attributes from the incoming ADD request. * * @param addOperation * Current ADD operation. */ private void processInboundRemove(final PreParseAddOperation addOperation) { final List<RawAttribute> inAttrs = new LinkedList<>(addOperation.getRawAttributes()); final ListIterator<RawAttribute> iterator = inAttrs.listIterator(); while (iterator.hasNext()) { final RawAttribute rawAttr = iterator.next(); final String attrName = toLowerCase(rawAttr.getAttributeType().trim()); if (attributesToRemove.contains(attrName)) { if (logger.isTraceEnabled()) { logger.trace("AttributeCleanupPlugin removing '%s'", rawAttr.getAttributeType()); } iterator.remove(); } } addOperation.setRawAttributes(inAttrs); } /** * Remove the attributes listed in the configuration under * ds-cfg-remove-inbound-attributes from the incoming MODIFY request. * * @param modifyOperation * Current MODIFY operation. */ private void processInboundRemove( final PreParseModifyOperation modifyOperation) { final List<RawModification> rawMods = new LinkedList<>(modifyOperation.getRawModifications()); final ListIterator<RawModification> iterator = rawMods.listIterator(); while (iterator.hasNext()) { final RawModification rawMod = iterator.next(); final RawAttribute rawAttr = rawMod.getAttribute(); final String attrName = toLowerCase(rawAttr.getAttributeType().trim()); if (attributesToRemove.contains(attrName)) { if (logger.isTraceEnabled()) { logger.trace("AttributeCleanupPlugin removing '%s'", rawAttr.getAttributeType()); } iterator.remove(); } } modifyOperation.setRawModifications(rawMods); } /** * Map the incoming attributes to the local ones. * * @param addOperation * Current ADD operation. */ private void processInboundRename(final PreParseAddOperation addOperation) { final List<RawAttribute> inAttrs = new LinkedList<>(addOperation.getRawAttributes()); final ListIterator<RawAttribute> iterator = inAttrs.listIterator(); while (iterator.hasNext()) { final RawAttribute rawAttr = iterator.next(); final String fromName = toLowerCase(rawAttr.getAttributeType().trim()); final String toName = attributesToRename.get(fromName); if (toName != null) { if (logger.isTraceEnabled()) { logger.trace("AttributeCleanupPlugin renaming '%s' to '%s'", rawAttr.getAttributeType(), toName); } rawAttr.setAttributeType(toName); } } addOperation.setRawAttributes(inAttrs); } /** * Rename the attributes in the incoming MODIFY request to names that exist in * the local schema as defined in the configuration. * * @param modifyOperation * Current MODIFY operation. */ private void processInboundRename( final PreParseModifyOperation modifyOperation) { final List<RawModification> rawMods = new LinkedList<>(modifyOperation.getRawModifications()); final ListIterator<RawModification> iterator = rawMods.listIterator(); while (iterator.hasNext()) { final RawModification rawMod = iterator.next(); final RawAttribute rawAttr = rawMod.getAttribute(); final String fromName = toLowerCase(rawAttr.getAttributeType().trim()); final String toName = attributesToRename.get(fromName); if (toName != null) { if (logger.isTraceEnabled()) { logger.trace("AttributeCleanupPlugin renaming '%s' to '%s'", rawAttr.getAttributeType(), toName); } rawAttr.setAttributeType(toName); } } modifyOperation.setRawModifications(rawMods); } }