001/* 002 * Copyright 2016-2018 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright (C) 2016-2018 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.ThreadSafety; 040import 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) 088public 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 @Override() 204 public Entry transformEntry(final Entry e) 205 { 206 // If the provided entry was null, then just return null. 207 if (e == null) 208 { 209 return null; 210 } 211 212 213 // Get a parsed representation of the entry's DN. If we can't parse the DN 214 // for some reason, then leave it unaltered. If we can parse it, then 215 // perform any appropriate transformation. 216 DN newDN = null; 217 LinkedHashSet<ObjectPair<String,String>> omittedRDNValues = null; 218 try 219 { 220 final DN dn = e.getParsedDN(); 221 222 if (dn.isDescendantOf(flattenBaseDN, false)) 223 { 224 // If the entry matches the exclude filter, then return null to indicate 225 // that the entry should be omitted from the results. 226 try 227 { 228 if ((excludeFilter != null) && excludeFilter.matchesEntry(e)) 229 { 230 return null; 231 } 232 } 233 catch (final Exception ex) 234 { 235 Debug.debugException(ex); 236 } 237 238 239 // If appropriate allocate a set to hold omitted RDN values. 240 if (addOmittedRDNAttributesToEntry || addOmittedRDNAttributesToRDN) 241 { 242 omittedRDNValues = new LinkedHashSet<>(5); 243 } 244 245 246 // Transform the parsed DN. 247 newDN = transformDN(dn, omittedRDNValues); 248 } 249 } 250 catch (final Exception ex) 251 { 252 Debug.debugException(ex); 253 return e; 254 } 255 256 257 // Iterate through the attributes and apply any appropriate transformations. 258 // If the resulting RDN should reflect any omitted RDNs, then create a 259 // temporary set to use to hold the RDN values omitted from attribute 260 // values. 261 final Collection<Attribute> originalAttributes = e.getAttributes(); 262 final ArrayList<Attribute> newAttributes = 263 new ArrayList<>(originalAttributes.size()); 264 265 final LinkedHashSet<ObjectPair<String,String>> tempOmittedRDNValues; 266 if (addOmittedRDNAttributesToRDN) 267 { 268 tempOmittedRDNValues = new LinkedHashSet<>(5); 269 } 270 else 271 { 272 tempOmittedRDNValues = null; 273 } 274 275 for (final Attribute a : originalAttributes) 276 { 277 newAttributes.add(transformAttribute(a, tempOmittedRDNValues)); 278 } 279 280 281 // Create the new entry. 282 final Entry newEntry; 283 if (newDN == null) 284 { 285 newEntry = new Entry(e.getDN(), schema, newAttributes); 286 } 287 else 288 { 289 newEntry = new Entry(newDN, schema, newAttributes); 290 } 291 292 293 // If we should add omitted RDN name-value pairs to the entry, then add them 294 // now. 295 if (addOmittedRDNAttributesToEntry && (omittedRDNValues != null)) 296 { 297 for (final ObjectPair<String,String> p : omittedRDNValues) 298 { 299 newEntry.addAttribute( 300 new Attribute(p.getFirst(), schema, p.getSecond())); 301 } 302 } 303 304 305 return newEntry; 306 } 307 308 309 310 /** 311 * Applies the appropriate transformation to the provided DN. 312 * 313 * @param dn The DN to transform. It must not be 314 * {@code null}. 315 * @param omittedRDNValues A set into which any omitted RDN values should be 316 * added. It may be {@code null} if we don't need 317 * to collect the set of omitted RDNs. 318 * 319 * @return The transformed DN, or the original DN if no alteration is 320 * necessary. 321 */ 322 private DN transformDN(final DN dn, 323 final Set<ObjectPair<String,String>> omittedRDNValues) 324 { 325 // Get the number of RDNs to omit. If we shouldn't omit any, then return 326 // the provided DN without alterations. 327 final RDN[] originalRDNs = dn.getRDNs(); 328 final int numRDNsToOmit = originalRDNs.length - flattenBaseRDNs.length - 1; 329 if (numRDNsToOmit == 0) 330 { 331 return dn; 332 } 333 334 335 // Construct an array of the new RDNs to use for the entry. 336 final RDN[] newRDNs = new RDN[flattenBaseRDNs.length + 1]; 337 System.arraycopy(flattenBaseRDNs, 0, newRDNs, 1, flattenBaseRDNs.length); 338 339 340 // If necessary, get the name-value pairs for the omitted RDNs and construct 341 // the new RDN. Otherwise, just preserve the original RDN. 342 if (omittedRDNValues == null) 343 { 344 newRDNs[0] = originalRDNs[0]; 345 } 346 else 347 { 348 for (int i=1; i <= numRDNsToOmit; i++) 349 { 350 final String[] names = originalRDNs[i].getAttributeNames(); 351 final String[] values = originalRDNs[i].getAttributeValues(); 352 for (int j=0; j < names.length; j++) 353 { 354 omittedRDNValues.add(new ObjectPair<>(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(new ObjectPair<>(origNames[i], origValues[i])); 365 } 366 367 // If we should include omitted RDN values in the new RDN, then construct 368 // a new RDN for the entry. Otherwise, preserve the original RDN. 369 if (addOmittedRDNAttributesToRDN) 370 { 371 final String[] originalRDNNames = originalRDNs[0].getAttributeNames(); 372 final String[] originalRDNValues = originalRDNs[0].getAttributeValues(); 373 374 final String[] newRDNNames = 375 new String[originalRDNNames.length + omittedRDNValues.size()]; 376 final String[] newRDNValues = new String[newRDNNames.length]; 377 378 int i=0; 379 for (int j=0; j < originalRDNNames.length; j++) 380 { 381 newRDNNames[i] = originalRDNNames[i]; 382 newRDNValues[i] = originalRDNValues[i]; 383 i++; 384 } 385 386 for (final ObjectPair<String,String> p : omittedRDNValues) 387 { 388 newRDNNames[i] = p.getFirst(); 389 newRDNValues[i] = p.getSecond(); 390 i++; 391 } 392 393 newRDNs[0] = new RDN(newRDNNames, newRDNValues, schema); 394 } 395 else 396 { 397 newRDNs[0] = originalRDNs[0]; 398 } 399 } 400 401 return new DN(newRDNs); 402 } 403 404 405 406 /** 407 * Applies the appropriate transformation to any values of the provided 408 * attribute that represent DNs. 409 * 410 * @param a The attribute to transform. It must not be 411 * {@code null}. 412 * @param omittedRDNValues A set into which any omitted RDN values should be 413 * added. It may be {@code null} if we don't need 414 * to collect the set of omitted RDNs. 415 * 416 * @return The transformed attribute, or the original attribute if no 417 * alteration is necessary. 418 */ 419 private Attribute transformAttribute(final Attribute a, 420 final Set<ObjectPair<String,String>> omittedRDNValues) 421 { 422 // Assume that the attribute doesn't have any values that are DNs, and that 423 // we won't need to create a new attribute. This should be the common case. 424 // Also, even if the attribute has one or more DNs, we don't need to do 425 // anything for values that aren't below the flatten base DN. 426 boolean hasTransformableDN = false; 427 final String[] values = a.getValues(); 428 for (final String value : values) 429 { 430 try 431 { 432 final DN dn = new DN(value); 433 if (dn.isDescendantOf(flattenBaseDN, false)) 434 { 435 hasTransformableDN = true; 436 break; 437 } 438 } 439 catch (final Exception e) 440 { 441 // This is the common case. We shouldn't even debug this. 442 } 443 } 444 445 if (! hasTransformableDN) 446 { 447 return a; 448 } 449 450 451 // If we've gotten here, then we know that the attribute has at least one 452 // value to be transformed. 453 final String[] newValues = new String[values.length]; 454 for (int i=0; i < values.length; i++) 455 { 456 try 457 { 458 final DN dn = new DN(values[i]); 459 if (dn.isDescendantOf(flattenBaseDN, false)) 460 { 461 if (omittedRDNValues != null) 462 { 463 omittedRDNValues.clear(); 464 } 465 newValues[i] = transformDN(dn, omittedRDNValues).toString(); 466 } 467 else 468 { 469 newValues[i] = values[i]; 470 } 471 } 472 catch (final Exception e) 473 { 474 // Even if some values are DNs, there may be values that aren't. Don't 475 // worry about this. Just use the existing value without alteration. 476 newValues[i] = values[i]; 477 } 478 } 479 480 return new Attribute(a.getName(), schema, newValues); 481 } 482 483 484 485 /** 486 * {@inheritDoc} 487 */ 488 @Override() 489 public Entry translate(final Entry original, final long firstLineNumber) 490 { 491 return transformEntry(original); 492 } 493 494 495 496 /** 497 * {@inheritDoc} 498 */ 499 @Override() 500 public Entry translateEntryToWrite(final Entry original) 501 { 502 return transformEntry(original); 503 } 504}