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