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