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