001/*
002 * Copyright 2016-2020 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright 2016-2020 Ping Identity Corporation
007 *
008 * Licensed under the Apache License, Version 2.0 (the "License");
009 * you may not use this file except in compliance with the License.
010 * You may obtain a copy of the License at
011 *
012 *    http://www.apache.org/licenses/LICENSE-2.0
013 *
014 * Unless required by applicable law or agreed to in writing, software
015 * distributed under the License is distributed on an "AS IS" BASIS,
016 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
017 * See the License for the specific language governing permissions and
018 * limitations under the License.
019 */
020/*
021 * Copyright (C) 2016-2020 Ping Identity Corporation
022 *
023 * This program is free software; you can redistribute it and/or modify
024 * it under the terms of the GNU General Public License (GPLv2 only)
025 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
026 * as published by the Free Software Foundation.
027 *
028 * This program is distributed in the hope that it will be useful,
029 * but WITHOUT ANY WARRANTY; without even the implied warranty of
030 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
031 * GNU General Public License for more details.
032 *
033 * You should have received a copy of the GNU General Public License
034 * along with this program; if not, see <http://www.gnu.org/licenses>.
035 */
036package com.unboundid.ldap.sdk.transformations;
037
038
039
040import java.io.Serializable;
041import java.util.ArrayList;
042import java.util.Collection;
043import java.util.LinkedHashSet;
044import java.util.Set;
045
046import com.unboundid.ldap.sdk.Attribute;
047import com.unboundid.ldap.sdk.DN;
048import com.unboundid.ldap.sdk.Entry;
049import com.unboundid.ldap.sdk.Filter;
050import com.unboundid.ldap.sdk.RDN;
051import com.unboundid.ldap.sdk.schema.Schema;
052import com.unboundid.util.Debug;
053import com.unboundid.util.ObjectPair;
054import com.unboundid.util.StaticUtils;
055import com.unboundid.util.ThreadSafety;
056import com.unboundid.util.ThreadSafetyLevel;
057
058
059
060/**
061 * This class provides an implementation of an entry transformation that will
062 * alter DNs below a specified base DN to ensure that they are exactly one level
063 * below the specified base DN.  This can be useful when migrating data
064 * containing a large number of branches into a flat DIT with all of the entries
065 * below a common parent.
066 * <BR><BR>
067 * Only entries that were previously more than one level below the base DN will
068 * be renamed.  The DN of the base entry itself will be unchanged, as well as
069 * the DNs of entries outside of the specified base DN.
070 * <BR><BR>
071 * For any entries that were originally more than one level below the specified
072 * base DN, any RDNs that were omitted may optionally be added as
073 * attributes to the updated entry.  For example, if the flatten base DN is
074 * "ou=People,dc=example,dc=com" and an entry is encountered with a DN of
075 * "uid=john.doe,ou=East,ou=People,dc=example,dc=com", the resulting DN would
076 * be "uid=john.doe,ou=People,dc=example,dc=com" and the entry may optionally be
077 * updated to include an "ou" attribute with a value of "East".
078 * <BR><BR>
079 * Alternately, the attribute-value pairs from any omitted RDNs may be added to
080 * the resulting entry's RDN, making it a multivalued RDN if necessary.  Using
081 * the example above, this means that the resulting DN could be
082 * "uid=john.doe+ou=East,ou=People,dc=example,dc=com".  This can help avoid the
083 * potential for naming conflicts if entries exist with the same RDN in
084 * different branches.
085 * <BR><BR>
086 * This transformation will also be applied to DNs used as attribute values in
087 * the entries to be processed.  All attributes in all entries (regardless of
088 * location in the DIT) will be examined, and any value that is a DN will have
089 * the same flattening transformation described above applied to it.  The
090 * processing will be applied to any entry anywhere in the DIT, but will only
091 * affect values that represent DNs below the flatten base DN.
092 * <BR><BR>
093 * In many cases, when flattening a DIT with a large number of branches, the
094 * non-leaf entries below the flatten base DN are often simple container entries
095 * like organizationalUnit entries without any real attributes.  In those cases,
096 * those container entries may no longer be necessary in the flattened DIT, and
097 * it may be desirable to eliminate them.  To address that, it is possible to
098 * provide a filter that can be used to identify these entries so that they can
099 * be excluded from the resulting LDIF output.  Note that only entries below the
100 * flatten base DN may be excluded by this transformation.  Any entry at or
101 * outside the specified base DN that matches the filter will be preserved.
102 */
103@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE)
104public final class FlattenSubtreeTransformation
105       implements EntryTransformation, Serializable
106{
107  /**
108   * The serial version UID for this serializable class.
109   */
110  private static final long serialVersionUID = -5500436195237056110L;
111
112
113
114  // Indicates whether the attribute-value pairs from any omitted RDNs should be
115  // added to any entries that are updated.
116  private final boolean addOmittedRDNAttributesToEntry;
117
118  // Indicates whether the RDN of the attribute-value pairs from any omitted
119  // RDNs should be added into the RDN for any entries that are updated.
120  private final boolean addOmittedRDNAttributesToRDN;
121
122  // The base DN below which to flatten the DIT.
123  private final DN flattenBaseDN;
124
125  // A filter that can be used to identify which entries to exclude.
126  private final Filter excludeFilter;
127
128  // The RDNs that comprise the flatten base DN.
129  private final RDN[] flattenBaseRDNs;
130
131  // The schema to use when processing.
132  private final Schema schema;
133
134
135
136  /**
137   * Creates a new instance of this transformation with the provided
138   * information.
139   *
140   * @param  schema                          The schema to use in processing.
141   *                                         It may be {@code null} if a default
142   *                                         standard schema should be used.
143   * @param  flattenBaseDN                   The base DN below which any
144   *                                         flattening will be performed.  In
145   *                                         the transformed data, all entries
146   *                                         below this base DN will be exactly
147   *                                         one level below this base DN.  It
148   *                                         must not be {@code null}.
149   * @param  addOmittedRDNAttributesToEntry  Indicates whether to add the
150   *                                         attribute-value pairs of any RDNs
151   *                                         stripped out of DNs during the
152   *                                         course of flattening the DIT should
153   *                                         be added as attribute values in the
154   *                                         target entry.
155   * @param  addOmittedRDNAttributesToRDN    Indicates whether to add the
156   *                                         attribute-value pairs of any RDNs
157   *                                         stripped out of DNs during the
158   *                                         course of flattening the DIT should
159   *                                         be added as additional values in
160   *                                         the RDN of the target entry (so the
161   *                                         resulting DN will have a
162   *                                         multivalued RDN with all of the
163   *                                         attribute-value pairs of the
164   *                                         original RDN, plus all
165   *                                         attribute-value pairs from any
166   *                                         omitted RDNs).
167   * @param  excludeFilter                   An optional filter that may be used
168   *                                         to exclude entries during the
169   *                                         flattening process.  If this is
170   *                                         non-{@code null}, then any entry
171   *                                         below the flatten base DN that
172   *                                         matches this filter will be
173   *                                         excluded from the results rather
174   *                                         than flattened.  This can be used
175   *                                         to strip out "container" entries
176   *                                         that were simply used to add levels
177   *                                         of hierarchy in the previous
178   *                                         branched DN that are no longer
179   *                                         needed in the flattened
180   *                                         representation of the DIT.
181   */
182  public FlattenSubtreeTransformation(final Schema schema,
183              final DN flattenBaseDN,
184              final boolean addOmittedRDNAttributesToEntry,
185              final boolean addOmittedRDNAttributesToRDN,
186              final Filter excludeFilter)
187  {
188    this.flattenBaseDN                  = flattenBaseDN;
189    this.addOmittedRDNAttributesToEntry = addOmittedRDNAttributesToEntry;
190    this.addOmittedRDNAttributesToRDN   = addOmittedRDNAttributesToRDN;
191    this.excludeFilter                  = excludeFilter;
192
193    flattenBaseRDNs = flattenBaseDN.getRDNs();
194
195
196    // If a schema was provided, then use it.  Otherwise, use the default
197    // standard schema.
198    Schema s = schema;
199    if (s == null)
200    {
201      try
202      {
203        s = Schema.getDefaultStandardSchema();
204      }
205      catch (final Exception e)
206      {
207        // This should never happen.
208        Debug.debugException(e);
209      }
210    }
211    this.schema = s;
212  }
213
214
215
216  /**
217   * {@inheritDoc}
218   */
219  @Override()
220  public Entry transformEntry(final Entry e)
221  {
222    // If the provided entry was null, then just return null.
223    if (e == null)
224    {
225      return null;
226    }
227
228
229    // Get a parsed representation of the entry's DN.  If we can't parse the DN
230    // for some reason, then leave it unaltered.  If we can parse it, then
231    // perform any appropriate transformation.
232    DN newDN = null;
233    LinkedHashSet<ObjectPair<String,String>> omittedRDNValues = null;
234    try
235    {
236      final DN dn = e.getParsedDN();
237
238      if (dn.isDescendantOf(flattenBaseDN, false))
239      {
240        // If the entry matches the exclude filter, then return null to indicate
241        // that the entry should be omitted from the results.
242        try
243        {
244          if ((excludeFilter != null) && excludeFilter.matchesEntry(e))
245          {
246            return null;
247          }
248        }
249        catch (final Exception ex)
250        {
251          Debug.debugException(ex);
252        }
253
254
255        // If appropriate allocate a set to hold omitted RDN values.
256        if (addOmittedRDNAttributesToEntry || addOmittedRDNAttributesToRDN)
257        {
258          omittedRDNValues =
259               new LinkedHashSet<>(StaticUtils.computeMapCapacity(5));
260        }
261
262
263        // Transform the parsed DN.
264        newDN = transformDN(dn, omittedRDNValues);
265      }
266    }
267    catch (final Exception ex)
268    {
269      Debug.debugException(ex);
270      return e;
271    }
272
273
274    // Iterate through the attributes and apply any appropriate transformations.
275    // If the resulting RDN should reflect any omitted RDNs, then create a
276    // temporary set to use to hold the RDN values omitted from attribute
277    // values.
278    final Collection<Attribute> originalAttributes = e.getAttributes();
279    final ArrayList<Attribute> newAttributes =
280         new ArrayList<>(originalAttributes.size());
281
282    final LinkedHashSet<ObjectPair<String,String>> tempOmittedRDNValues;
283    if (addOmittedRDNAttributesToRDN)
284    {
285      tempOmittedRDNValues =
286           new LinkedHashSet<>(StaticUtils.computeMapCapacity(5));
287    }
288    else
289    {
290      tempOmittedRDNValues = null;
291    }
292
293    for (final Attribute a : originalAttributes)
294    {
295      newAttributes.add(transformAttribute(a, tempOmittedRDNValues));
296    }
297
298
299    // Create the new entry.
300    final Entry newEntry;
301    if (newDN == null)
302    {
303      newEntry = new Entry(e.getDN(), schema, newAttributes);
304    }
305    else
306    {
307      newEntry = new Entry(newDN, schema, newAttributes);
308    }
309
310
311    // If we should add omitted RDN name-value pairs to the entry, then add them
312    // now.
313    if (addOmittedRDNAttributesToEntry && (omittedRDNValues != null))
314    {
315      for (final ObjectPair<String,String> p : omittedRDNValues)
316      {
317        newEntry.addAttribute(
318             new Attribute(p.getFirst(), schema, p.getSecond()));
319      }
320    }
321
322
323    return newEntry;
324  }
325
326
327
328  /**
329   * Applies the appropriate transformation to the provided DN.
330   *
331   * @param  dn                The DN to transform.  It must not be
332   *                           {@code null}.
333   * @param  omittedRDNValues  A set into which any omitted RDN values should be
334   *                           added.  It may be {@code null} if we don't need
335   *                           to collect the set of omitted RDNs.
336   *
337   * @return  The transformed DN, or the original DN if no alteration is
338   *          necessary.
339   */
340  private DN transformDN(final DN dn,
341                  final Set<ObjectPair<String,String>> omittedRDNValues)
342  {
343    // Get the number of RDNs to omit.  If we shouldn't omit any, then return
344    // the provided DN without alterations.
345    final RDN[] originalRDNs = dn.getRDNs();
346    final int numRDNsToOmit = originalRDNs.length - flattenBaseRDNs.length - 1;
347    if (numRDNsToOmit == 0)
348    {
349      return dn;
350    }
351
352
353    // Construct an array of the new RDNs to use for the entry.
354    final RDN[] newRDNs = new RDN[flattenBaseRDNs.length + 1];
355    System.arraycopy(flattenBaseRDNs, 0, newRDNs, 1, flattenBaseRDNs.length);
356
357
358    // If necessary, get the name-value pairs for the omitted RDNs and construct
359    // the new RDN.  Otherwise, just preserve the original RDN.
360    if (omittedRDNValues == null)
361    {
362      newRDNs[0] = originalRDNs[0];
363    }
364    else
365    {
366      for (int i=1; i <= numRDNsToOmit; i++)
367      {
368        final String[] names  = originalRDNs[i].getAttributeNames();
369        final String[] values = originalRDNs[i].getAttributeValues();
370        for (int j=0; j < names.length; j++)
371        {
372          omittedRDNValues.add(new ObjectPair<>(names[j], values[j]));
373        }
374      }
375
376      // Just in case the entry's original RDN has one or more name-value pairs
377      // as some of the omitted RDNs, remove those values from the set.
378      final String[] origNames  = originalRDNs[0].getAttributeNames();
379      final String[] origValues = originalRDNs[0].getAttributeValues();
380      for (int i=0; i < origNames.length; i++)
381      {
382        omittedRDNValues.remove(new ObjectPair<>(origNames[i], origValues[i]));
383      }
384
385      // If we should include omitted RDN values in the new RDN, then construct
386      // a new RDN for the entry.  Otherwise, preserve the original RDN.
387      if (addOmittedRDNAttributesToRDN)
388      {
389        final String[] originalRDNNames  = originalRDNs[0].getAttributeNames();
390        final String[] originalRDNValues = originalRDNs[0].getAttributeValues();
391
392        final String[] newRDNNames =
393             new String[originalRDNNames.length + omittedRDNValues.size()];
394        final String[] newRDNValues = new String[newRDNNames.length];
395
396        int i=0;
397        for (int j=0; j < originalRDNNames.length; j++)
398        {
399          newRDNNames[i]  = originalRDNNames[i];
400          newRDNValues[i] = originalRDNValues[i];
401          i++;
402        }
403
404        for (final ObjectPair<String,String> p : omittedRDNValues)
405        {
406          newRDNNames[i]  = p.getFirst();
407          newRDNValues[i] = p.getSecond();
408          i++;
409        }
410
411        newRDNs[0] = new RDN(newRDNNames, newRDNValues, schema);
412      }
413      else
414      {
415        newRDNs[0] = originalRDNs[0];
416      }
417    }
418
419    return new DN(newRDNs);
420  }
421
422
423
424  /**
425   * Applies the appropriate transformation to any values of the provided
426   * attribute that represent DNs.
427   *
428   * @param  a                 The attribute to transform.  It must not be
429   *                           {@code null}.
430   * @param  omittedRDNValues  A set into which any omitted RDN values should be
431   *                           added.  It may be {@code null} if we don't need
432   *                           to collect the set of omitted RDNs.
433   *
434   * @return  The transformed attribute, or the original attribute if no
435   *          alteration is necessary.
436   */
437  private Attribute transformAttribute(final Attribute a,
438                         final Set<ObjectPair<String,String>> omittedRDNValues)
439  {
440    // Assume that the attribute doesn't have any values that are DNs, and that
441    // we won't need to create a new attribute.  This should be the common case.
442    // Also, even if the attribute has one or more DNs, we don't need to do
443    // anything for values that aren't below the flatten base DN.
444    boolean hasTransformableDN = false;
445    final String[] values = a.getValues();
446    for (final String value : values)
447    {
448      try
449      {
450        final DN dn = new DN(value);
451        if (dn.isDescendantOf(flattenBaseDN, false))
452        {
453          hasTransformableDN = true;
454          break;
455        }
456      }
457      catch (final Exception e)
458      {
459        // This is the common case.  We shouldn't even debug this.
460      }
461    }
462
463    if (! hasTransformableDN)
464    {
465      return a;
466    }
467
468
469    // If we've gotten here, then we know that the attribute has at least one
470    // value to be transformed.
471    final String[] newValues = new String[values.length];
472    for (int i=0; i < values.length; i++)
473    {
474      try
475      {
476        final DN dn = new DN(values[i]);
477        if (dn.isDescendantOf(flattenBaseDN, false))
478        {
479          if (omittedRDNValues != null)
480          {
481            omittedRDNValues.clear();
482          }
483          newValues[i] = transformDN(dn, omittedRDNValues).toString();
484        }
485        else
486        {
487          newValues[i] = values[i];
488        }
489      }
490      catch (final Exception e)
491      {
492        // Even if some values are DNs, there may be values that aren't.  Don't
493        // worry about this.  Just use the existing value without alteration.
494        newValues[i] = values[i];
495      }
496    }
497
498    return new Attribute(a.getName(), schema, newValues);
499  }
500
501
502
503  /**
504   * {@inheritDoc}
505   */
506  @Override()
507  public Entry translate(final Entry original, final long firstLineNumber)
508  {
509    return transformEntry(original);
510  }
511
512
513
514  /**
515   * {@inheritDoc}
516   */
517  @Override()
518  public Entry translateEntryToWrite(final Entry original)
519  {
520    return transformEntry(original);
521  }
522}