001/* 002 * Copyright 2016-2017 UnboundID Corp. 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright (C) 2016-2017 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 */ 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 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}