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