001/* 002 * CDDL HEADER START 003 * 004 * The contents of this file are subject to the terms of the 005 * Common Development and Distribution License, Version 1.0 only 006 * (the "License"). You may not use this file except in compliance 007 * with the License. 008 * 009 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt 010 * or http://forgerock.org/license/CDDLv1.0.html. 011 * See the License for the specific language governing permissions 012 * and limitations under the License. 013 * 014 * When distributing Covered Code, include this CDDL HEADER in each 015 * file and include the License file at legal-notices/CDDLv1_0.txt. 016 * If applicable, add the following below this CDDL HEADER, with the 017 * fields enclosed by brackets "[]" replaced with your own identifying 018 * information: 019 * Portions Copyright [yyyy] [name of copyright owner] 020 * 021 * CDDL HEADER END 022 * 023 * 024 * Copyright 2011 profiq s.r.o. 025 * Portions Copyright 2011-2015 ForgeRock AS. 026 */ 027package org.opends.server.plugins; 028 029import static org.opends.messages.PluginMessages.*; 030import static com.forgerock.opendj.util.StaticUtils.toLowerCase; 031 032import java.util.*; 033import java.util.concurrent.locks.ReentrantReadWriteLock; 034import java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock; 035import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock; 036 037import org.forgerock.i18n.LocalizableMessage; 038import org.forgerock.i18n.slf4j.LocalizedLogger; 039import org.forgerock.opendj.config.server.ConfigChangeResult; 040import org.forgerock.opendj.config.server.ConfigException; 041import org.forgerock.opendj.ldap.ResultCode; 042import org.opends.server.admin.server.ConfigurationChangeListener; 043import org.opends.server.admin.std.server.AttributeCleanupPluginCfg; 044import org.opends.server.admin.std.server.PluginCfg; 045import org.opends.server.api.plugin.DirectoryServerPlugin; 046import org.opends.server.api.plugin.PluginResult; 047import org.opends.server.api.plugin.PluginType; 048import org.opends.server.core.DirectoryServer; 049import org.opends.server.types.*; 050import org.opends.server.types.operation.PreParseAddOperation; 051import org.opends.server.types.operation.PreParseModifyOperation; 052 053/** 054 * The attribute cleanup plugin implementation class. The plugin removes and/or 055 * renames the configured parameters from the incoming ADD and MODIFY requests. 056 */ 057public class AttributeCleanupPlugin extends 058 DirectoryServerPlugin<AttributeCleanupPluginCfg> implements 059 ConfigurationChangeListener<AttributeCleanupPluginCfg> 060{ 061 062 /** 063 * Plugin configuration. 064 */ 065 private AttributeCleanupPluginCfg config; 066 067 /** 068 * Debug tracer. 069 */ 070 private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); 071 072 /** 073 * A table of attributes to be renamed. 074 */ 075 private Map<String, String> attributesToRename; 076 077 /** 078 * The set of attributes to be removed. 079 */ 080 private Set<String> attributesToRemove; 081 082 /** 083 * This lock prevents concurrent updates to the configuration while operations 084 * are being processed. 085 */ 086 private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); 087 private final ReadLock sharedLock = lock.readLock(); 088 private final WriteLock exclusiveLock = lock.writeLock(); 089 090 091 092 /** 093 * Default constructor. 094 */ 095 public AttributeCleanupPlugin() 096 { 097 super(); 098 } 099 100 101 102 /** {@inheritDoc} */ 103 @Override 104 public ConfigChangeResult applyConfigurationChange( 105 final AttributeCleanupPluginCfg config) 106 { 107 exclusiveLock.lock(); 108 try 109 { 110 /* Apply the change, as at this point is has been validated. */ 111 this.config = config; 112 113 attributesToRename = new HashMap<>(); 114 for (final String mapping : config.getRenameInboundAttributes()) 115 { 116 final int colonPos = mapping.lastIndexOf(":"); 117 final String fromAttr = mapping.substring(0, colonPos).trim(); 118 final String toAttr = mapping.substring(colonPos + 1).trim(); 119 attributesToRename.put(toLowerCase(fromAttr), toLowerCase(toAttr)); 120 } 121 122 attributesToRemove = new HashSet<>(); 123 for (final String attr : config.getRemoveInboundAttributes()) 124 { 125 attributesToRemove.add(toLowerCase(attr.trim())); 126 } 127 128 /* Update was successful, no restart required. */ 129 return new ConfigChangeResult(); 130 } 131 finally 132 { 133 exclusiveLock.unlock(); 134 } 135 } 136 137 138 139 /** {@inheritDoc} */ 140 @Override 141 public PluginResult.PreParse doPreParse( 142 final PreParseAddOperation addOperation) 143 { 144 sharedLock.lock(); 145 try 146 { 147 /* 148 * First strip the listed attributes, then rename the ones that remain. 149 */ 150 processInboundRemove(addOperation); 151 processInboundRename(addOperation); 152 153 return PluginResult.PreParse.continueOperationProcessing(); 154 } 155 finally 156 { 157 sharedLock.unlock(); 158 } 159 } 160 161 162 163 /** {@inheritDoc} */ 164 @Override 165 public PluginResult.PreParse doPreParse( 166 final PreParseModifyOperation modifyOperation) 167 { 168 sharedLock.lock(); 169 try 170 { 171 /* 172 * First strip the listed attributes, then rename the ones that remain. 173 */ 174 processInboundRemove(modifyOperation); 175 processInboundRename(modifyOperation); 176 177 /* 178 * If the MODIFY request has been stripped of ALL modifications, stop the 179 * processing and return SUCCESS to the client. 180 */ 181 if (modifyOperation.getRawModifications().isEmpty()) 182 { 183 if (logger.isTraceEnabled()) 184 { 185 logger.trace("The AttributeCleanupPlugin has eliminated all " 186 + "modifications. The processing should be stopped."); 187 } 188 return PluginResult.PreParse.stopProcessing(ResultCode.SUCCESS, null); 189 } 190 191 return PluginResult.PreParse.continueOperationProcessing(); 192 } 193 finally 194 { 195 sharedLock.unlock(); 196 } 197 } 198 199 200 201 /** {@inheritDoc} */ 202 @Override 203 public void finalizePlugin() 204 { 205 /* 206 * It's not essential to take the lock here, but we will anyhow for 207 * consistency with other methods. 208 */ 209 exclusiveLock.lock(); 210 try 211 { 212 /* Deregister change listeners. */ 213 config.removeAttributeCleanupChangeListener(this); 214 } 215 finally 216 { 217 exclusiveLock.unlock(); 218 } 219 } 220 221 222 223 /** {@inheritDoc} */ 224 @Override 225 public void initializePlugin(final Set<PluginType> pluginTypes, 226 final AttributeCleanupPluginCfg configuration) throws ConfigException, 227 InitializationException 228 { 229 /* 230 * The plugin should be invoked only for pre-parse ADD and MODIFY 231 * operations. 232 */ 233 for (final PluginType t : pluginTypes) 234 { 235 switch (t) 236 { 237 case PRE_PARSE_ADD: 238 break; 239 case PRE_PARSE_MODIFY: 240 break; 241 default: 242 throw new ConfigException(ERR_PLUGIN_ATTR_CLEANUP_INITIALIZE_PLUGIN.get(t)); 243 } 244 } 245 246 /* Verify the current configuration. */ 247 final List<LocalizableMessage> messages = new LinkedList<>(); 248 if (!isConfigurationChangeAcceptable(configuration, messages)) 249 { 250 throw new ConfigException(messages.get(0)); 251 } 252 253 /* Register change listeners. */ 254 configuration.addAttributeCleanupChangeListener(this); 255 256 /* Save the configuration. */ 257 applyConfigurationChange(configuration); 258 } 259 260 261 262 /** {@inheritDoc} */ 263 @Override 264 public boolean isConfigurationAcceptable(final PluginCfg configuration, 265 final List<LocalizableMessage> unacceptableReasons) 266 { 267 final AttributeCleanupPluginCfg cfg = 268 (AttributeCleanupPluginCfg) configuration; 269 return isConfigurationChangeAcceptable(cfg, unacceptableReasons); 270 } 271 272 273 274 /** {@inheritDoc} */ 275 @Override 276 public boolean isConfigurationChangeAcceptable( 277 final AttributeCleanupPluginCfg config, final List<LocalizableMessage> messages) 278 { 279 /* 280 * The admin framework will ensure that there are no duplicate attributes to 281 * be removed. 282 */ 283 boolean isValid = true; 284 285 /* 286 * Verify that there are no duplicate mappings and that attributes are 287 * renamed to valid attribute types. 288 */ 289 final Set<String> fromAttrs = new HashSet<>(); 290 for (final String attr : config.getRenameInboundAttributes()) 291 { 292 /* 293 * The format is: from:to where each 'from' and 'to' are attribute 294 * descriptions. The admin framework ensures that the format is correct. 295 */ 296 final int colonPos = attr.lastIndexOf(":"); 297 final String fromAttr = attr.substring(0, colonPos).trim(); 298 final String toAttr = attr.substring(colonPos + 1).trim(); 299 300 /* 301 * Make sure that toAttr is defined within the server, being careful to 302 * ignore attribute options. 303 */ 304 final int semicolonPos = toAttr.indexOf(";"); 305 final String toAttrType = semicolonPos < 0 && semicolonPos < toAttr.length() - 1 306 ? toAttr 307 : toAttr.substring(semicolonPos + 1); 308 309 if (DirectoryServer.getAttributeTypeOrNull(toLowerCase(toAttrType)) == null) 310 { 311 messages.add(ERR_PLUGIN_ATTR_CLEANUP_ATTRIBUTE_MISSING.get(toAttr)); 312 isValid = false; 313 } 314 315 // Check for duplicates. 316 final String nfromAttr = toLowerCase(fromAttr); 317 if (!fromAttrs.add(nfromAttr)) 318 { 319 messages.add(ERR_PLUGIN_ATTR_CLEANUP_DUPLICATE_VALUE.get(fromAttr)); 320 isValid = false; 321 } 322 323 // Check that attribute does not map to itself. 324 if (nfromAttr.equals(toLowerCase(toAttr))) 325 { 326 messages.add(ERR_PLUGIN_ATTR_CLEANUP_EQUAL_VALUES.get(fromAttr, toAttr)); 327 isValid = false; 328 } 329 } 330 331 return isValid; 332 } 333 334 335 336 /** 337 * Remove the attributes listed in the configuration under 338 * ds-cfg-remove-inbound-attributes from the incoming ADD request. 339 * 340 * @param addOperation 341 * Current ADD operation. 342 */ 343 private void processInboundRemove(final PreParseAddOperation addOperation) 344 { 345 final List<RawAttribute> inAttrs = new LinkedList<>(addOperation.getRawAttributes()); 346 final ListIterator<RawAttribute> iterator = inAttrs.listIterator(); 347 while (iterator.hasNext()) 348 { 349 final RawAttribute rawAttr = iterator.next(); 350 final String attrName = toLowerCase(rawAttr.getAttributeType().trim()); 351 if (attributesToRemove.contains(attrName)) 352 { 353 if (logger.isTraceEnabled()) 354 { 355 logger.trace("AttributeCleanupPlugin removing '%s'", 356 rawAttr.getAttributeType()); 357 } 358 iterator.remove(); 359 } 360 } 361 addOperation.setRawAttributes(inAttrs); 362 } 363 364 365 366 /** 367 * Remove the attributes listed in the configuration under 368 * ds-cfg-remove-inbound-attributes from the incoming MODIFY request. 369 * 370 * @param modifyOperation 371 * Current MODIFY operation. 372 */ 373 private void processInboundRemove( 374 final PreParseModifyOperation modifyOperation) 375 { 376 final List<RawModification> rawMods = new LinkedList<>(modifyOperation.getRawModifications()); 377 final ListIterator<RawModification> iterator = rawMods.listIterator(); 378 while (iterator.hasNext()) 379 { 380 final RawModification rawMod = iterator.next(); 381 final RawAttribute rawAttr = rawMod.getAttribute(); 382 final String attrName = toLowerCase(rawAttr.getAttributeType().trim()); 383 if (attributesToRemove.contains(attrName)) 384 { 385 if (logger.isTraceEnabled()) 386 { 387 logger.trace("AttributeCleanupPlugin removing '%s'", 388 rawAttr.getAttributeType()); 389 } 390 iterator.remove(); 391 } 392 } 393 modifyOperation.setRawModifications(rawMods); 394 } 395 396 397 398 /** 399 * Map the incoming attributes to the local ones. 400 * 401 * @param addOperation 402 * Current ADD operation. 403 */ 404 private void processInboundRename(final PreParseAddOperation addOperation) 405 { 406 final List<RawAttribute> inAttrs = new LinkedList<>(addOperation.getRawAttributes()); 407 final ListIterator<RawAttribute> iterator = inAttrs.listIterator(); 408 while (iterator.hasNext()) 409 { 410 final RawAttribute rawAttr = iterator.next(); 411 final String fromName = toLowerCase(rawAttr.getAttributeType().trim()); 412 final String toName = attributesToRename.get(fromName); 413 if (toName != null) 414 { 415 if (logger.isTraceEnabled()) 416 { 417 logger.trace("AttributeCleanupPlugin renaming '%s' to '%s'", 418 rawAttr.getAttributeType(), toName); 419 } 420 rawAttr.setAttributeType(toName); 421 } 422 } 423 addOperation.setRawAttributes(inAttrs); 424 } 425 426 427 428 /** 429 * Rename the attributes in the incoming MODIFY request to names that exist in 430 * the local schema as defined in the configuration. 431 * 432 * @param modifyOperation 433 * Current MODIFY operation. 434 */ 435 private void processInboundRename( 436 final PreParseModifyOperation modifyOperation) 437 { 438 final List<RawModification> rawMods = new LinkedList<>(modifyOperation.getRawModifications()); 439 final ListIterator<RawModification> iterator = rawMods.listIterator(); 440 while (iterator.hasNext()) 441 { 442 final RawModification rawMod = iterator.next(); 443 final RawAttribute rawAttr = rawMod.getAttribute(); 444 final String fromName = toLowerCase(rawAttr.getAttributeType().trim()); 445 final String toName = attributesToRename.get(fromName); 446 if (toName != null) 447 { 448 if (logger.isTraceEnabled()) 449 { 450 logger.trace("AttributeCleanupPlugin renaming '%s' to '%s'", 451 rawAttr.getAttributeType(), toName); 452 } 453 rawAttr.setAttributeType(toName); 454 } 455 } 456 modifyOperation.setRawModifications(rawMods); 457 } 458}