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}