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