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.util.ArrayList; 041import java.util.Collection; 042import java.util.Collections; 043import java.util.HashSet; 044import java.util.Set; 045import java.util.concurrent.atomic.AtomicLong; 046 047import com.unboundid.ldap.sdk.Attribute; 048import com.unboundid.ldap.sdk.DN; 049import com.unboundid.ldap.sdk.Entry; 050import com.unboundid.ldap.sdk.RDN; 051import com.unboundid.ldap.sdk.schema.AttributeTypeDefinition; 052import com.unboundid.ldap.sdk.schema.Schema; 053import com.unboundid.util.Debug; 054import com.unboundid.util.NotNull; 055import com.unboundid.util.Nullable; 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 * replace the existing set of values for a given attribute with a value that 065 * contains a numeric counter (optionally along with additional static text) 066 * that increments for each entry that contains the target attribute. The 067 * resulting attribute will only have a single value, even if it originally had 068 * multiple values. 069 */ 070@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE) 071public final class ReplaceWithCounterTransformation 072 implements EntryTransformation 073{ 074 // The counter to use to obtain the values. 075 @NotNull private final AtomicLong counter; 076 077 // Indicates whether to update the DN of the target entry if its RDN includes 078 // the target attribute. 079 private final boolean replaceInRDN; 080 081 // The amount by which to increment the counter for each entry. 082 private final long incrementAmount; 083 084 // The schema to use when processing. 085 @Nullable private final Schema schema; 086 087 // The names that may be used to reference the attribute to replace. 088 @NotNull private final Set<String> names; 089 090 // The static text that will appear after the number in generated values. 091 @Nullable private final String afterText; 092 093 // The static text that will appear before the number in generated values. 094 @Nullable private final String beforeText; 095 096 097 098 /** 099 * Creates a new replace with counter transformation using the provided 100 * information. 101 * 102 * @param schema The schema to use to identify alternate names for 103 * the target attribute. This may be {@code null} if 104 * a default standard schema should be used. 105 * @param attributeName The name of the attribute that should be replaced 106 * with the generated value. 107 * @param initialValue The initial value to use for the counter. 108 * @param incrementAmount The amount by which the counter should be 109 * incremented for each entry containing the target 110 * attribute. 111 * @param beforeText An optional string that should appear before the 112 * counter in generated values. It may be 113 * {@code null} if no before text should be used. 114 * @param afterText An optional string that should appear after the 115 * counter in generated values. It may be 116 * {@code null} if no after text should be used. 117 * @param replaceInRDN Indicates whether to update the DN of the target 118 * entry if its RDN includes the target attribute. 119 */ 120 public ReplaceWithCounterTransformation(@Nullable final Schema schema, 121 @NotNull final String attributeName, 122 final long initialValue, 123 final long incrementAmount, 124 @Nullable final String beforeText, 125 @Nullable final String afterText, 126 final boolean replaceInRDN) 127 { 128 this.incrementAmount = incrementAmount; 129 this.replaceInRDN = replaceInRDN; 130 131 counter = new AtomicLong(initialValue); 132 133 if (beforeText == null) 134 { 135 this.beforeText = ""; 136 } 137 else 138 { 139 this.beforeText = beforeText; 140 } 141 142 if (afterText == null) 143 { 144 this.afterText = ""; 145 } 146 else 147 { 148 this.afterText = afterText; 149 } 150 151 152 // If a schema was provided, then use it. Otherwise, use the default 153 // standard schema. 154 Schema s = schema; 155 if (s == null) 156 { 157 try 158 { 159 s = Schema.getDefaultStandardSchema(); 160 } 161 catch (final Exception e) 162 { 163 // This should never happen. 164 Debug.debugException(e); 165 } 166 } 167 this.schema = s; 168 169 170 // Get all names that can be used to reference the target attribute. 171 final HashSet<String> nameSet = 172 new HashSet<>(StaticUtils.computeMapCapacity(5)); 173 final String baseName = 174 StaticUtils.toLowerCase(Attribute.getBaseName(attributeName)); 175 nameSet.add(baseName); 176 if (s != null) 177 { 178 final AttributeTypeDefinition at = s.getAttributeType(baseName); 179 if (at != null) 180 { 181 nameSet.add(StaticUtils.toLowerCase(at.getOID())); 182 for (final String name : at.getNames()) 183 { 184 nameSet.add(StaticUtils.toLowerCase(name)); 185 } 186 } 187 } 188 names = Collections.unmodifiableSet(nameSet); 189 } 190 191 192 193 /** 194 * {@inheritDoc} 195 */ 196 @Override() 197 @Nullable() 198 public Entry transformEntry(@NotNull final Entry e) 199 { 200 if (e == null) 201 { 202 return null; 203 } 204 205 206 // See if the DN contains the target attribute in the RDN. If so, then 207 // replace its value. 208 String dn = e.getDN(); 209 String newValue = null; 210 if (replaceInRDN) 211 { 212 try 213 { 214 final DN parsedDN = new DN(dn); 215 final RDN rdn = parsedDN.getRDN(); 216 for (final String name : names) 217 { 218 if (rdn.hasAttribute(name)) 219 { 220 newValue = 221 beforeText + counter.getAndAdd(incrementAmount) + afterText; 222 break; 223 } 224 } 225 226 if (newValue != null) 227 { 228 if (rdn.isMultiValued()) 229 { 230 final String[] attrNames = rdn.getAttributeNames(); 231 final byte[][] originalValues = rdn.getByteArrayAttributeValues(); 232 final byte[][] newValues = new byte[originalValues.length][]; 233 for (int i=0; i < attrNames.length; i++) 234 { 235 if (names.contains(StaticUtils.toLowerCase(attrNames[i]))) 236 { 237 newValues[i] = StaticUtils.getBytes(newValue); 238 } 239 else 240 { 241 newValues[i] = originalValues[i]; 242 } 243 } 244 dn = new DN(new RDN(attrNames, newValues, schema), 245 parsedDN.getParent()).toString(); 246 } 247 else 248 { 249 dn = new DN(new RDN(rdn.getAttributeNames()[0], newValue, schema), 250 parsedDN.getParent()).toString(); 251 } 252 } 253 } 254 catch (final Exception ex) 255 { 256 Debug.debugException(ex); 257 } 258 } 259 260 261 // If the RDN doesn't contain the target attribute, then see if the entry 262 // contains the target attribute. If not, then just return the provided 263 // entry. 264 if (newValue == null) 265 { 266 boolean hasAttribute = false; 267 for (final String name : names) 268 { 269 if (e.hasAttribute(name)) 270 { 271 hasAttribute = true; 272 break; 273 } 274 } 275 276 if (! hasAttribute) 277 { 278 return e; 279 } 280 } 281 282 283 // If we haven't computed the new value for this entry, then do so now. 284 if (newValue == null) 285 { 286 newValue = beforeText + counter.getAndAdd(incrementAmount) + afterText; 287 } 288 289 290 // Iterate through the attributes in the entry and make the appropriate 291 // updates. 292 final Collection<Attribute> originalAttributes = e.getAttributes(); 293 final ArrayList<Attribute> updatedAttributes = 294 new ArrayList<>(originalAttributes.size()); 295 for (final Attribute a : originalAttributes) 296 { 297 if (names.contains(StaticUtils.toLowerCase(a.getBaseName()))) 298 { 299 updatedAttributes.add(new Attribute(a.getName(), schema, newValue)); 300 } 301 else 302 { 303 updatedAttributes.add(a); 304 } 305 } 306 307 308 // Return the updated entry. 309 return new Entry(dn, schema, updatedAttributes); 310 } 311 312 313 314 /** 315 * {@inheritDoc} 316 */ 317 @Override() 318 @Nullable() 319 public Entry translate(@NotNull final Entry original, 320 final long firstLineNumber) 321 { 322 return transformEntry(original); 323 } 324 325 326 327 /** 328 * {@inheritDoc} 329 */ 330 @Override() 331 @Nullable() 332 public Entry translateEntryToWrite(@NotNull final Entry original) 333 { 334 return transformEntry(original); 335 } 336}