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