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