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.Collections;
026    import java.util.HashSet;
027    import java.util.Set;
028    
029    import com.unboundid.ldap.sdk.Attribute;
030    import com.unboundid.ldap.sdk.DN;
031    import com.unboundid.ldap.sdk.Entry;
032    import com.unboundid.ldap.sdk.Filter;
033    import com.unboundid.ldap.sdk.SearchScope;
034    import com.unboundid.ldap.sdk.schema.AttributeTypeDefinition;
035    import com.unboundid.ldap.sdk.schema.Schema;
036    import com.unboundid.util.Debug;
037    import com.unboundid.util.StaticUtils;
038    import com.unboundid.util.ThreadSafety;
039    import com.unboundid.util.ThreadSafetyLevel;
040    
041    
042    
043    /**
044     * This class provides an implementation of an entry transformation that will
045     * add a specified attribute with a given set of values to any entry that does
046     * not already contain that attribute and matches a specified set of criteria.
047     */
048    @ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE)
049    public final class AddAttributeTransformation
050           implements EntryTransformation
051    {
052      // The attribute to add if appropriate.
053      private final Attribute attributeToAdd;
054    
055      // Indicates whether we need to check entries against the filter.
056      private final boolean examineFilter;
057    
058      // Indicates whether we need to check entries against the scope.
059      private final boolean examineScope;
060    
061      // Indicates whether to only add the attribute to entries that do not already
062      // have any values for the associated attribute type.
063      private final boolean onlyIfMissing;
064    
065      // The base DN to use to identify entries to which to add the attribute.
066      private final DN baseDN;
067    
068      // The filter to use to identify entries to which to add the attribute.
069      private final Filter filter;
070    
071      // The schema to use when processing.
072      private final Schema schema;
073    
074      // The scope to use to identify entries to which to add the attribute.
075      private final SearchScope scope;
076    
077      // The names that can be used to reference the target attribute.
078      private final Set<String> names;
079    
080    
081    
082      /**
083       * Creates a new add attribute transformation with the provided information.
084       *
085       * @param  schema          The schema to use in processing.  It may be
086       *                         {@code null} if a default standard schema should be
087       *                         used.
088       * @param  baseDN          The base DN to use to identify which entries to
089       *                         update.  If this is {@code null}, it will be
090       *                         assumed to be the null DN.
091       * @param  scope           The scope to use to identify which entries to
092       *                         update.  If this is {@code null}, it will be
093       *                         assumed to be {@link SearchScope#SUB}.
094       * @param  filter          An optional filter to use to identify which entries
095       *                         to update.  If this is {@code null}, then a default
096       *                         LDAP true filter (which will match any entry) will
097       *                         be used.
098       * @param  attributeToAdd  The attribute to add to entries that match the
099       *                         criteria and do not already contain any values for
100       *                         the specified attribute.  It must not be
101       *                         {@code null}.
102       * @param  onlyIfMissing   Indicates whether the attribute should only be
103       *                         added to entries that do not already contain it.
104       *                         If this is {@code false} and an entry that matches
105       *                         the base, scope, and filter criteria and already
106       *                         has one or more values for the target attribute
107       *                         will be updated to include the new values in
108       *                         addition to the existing values.
109       */
110      public AddAttributeTransformation(final Schema schema, final DN baseDN,
111                                        final SearchScope scope,
112                                        final Filter filter,
113                                        final Attribute attributeToAdd,
114                                        final boolean onlyIfMissing)
115      {
116        this.attributeToAdd = attributeToAdd;
117        this.onlyIfMissing = onlyIfMissing;
118    
119    
120        // If a schema was provided, then use it.  Otherwise, use the default
121        // standard schema.
122        Schema s = schema;
123        if (s == null)
124        {
125          try
126          {
127            s = Schema.getDefaultStandardSchema();
128          }
129          catch (final Exception e)
130          {
131            // This should never happen.
132            Debug.debugException(e);
133          }
134        }
135        this.schema = s;
136    
137    
138        // Identify all of the names that can be used to reference the specified
139        // attribute.
140        final HashSet<String> attrNames = new HashSet<String>(5);
141        final String baseName =
142             StaticUtils.toLowerCase(attributeToAdd.getBaseName());
143        attrNames.add(baseName);
144        if (s != null)
145        {
146          final AttributeTypeDefinition at = s.getAttributeType(baseName);
147          if (at != null)
148          {
149            attrNames.add(StaticUtils.toLowerCase(at.getOID()));
150            for (final String name : at.getNames())
151            {
152              attrNames.add(StaticUtils.toLowerCase(name));
153            }
154          }
155        }
156        names = Collections.unmodifiableSet(attrNames);
157    
158    
159        // If a base DN was provided, then use it.  Otherwise, use the null DN.
160        if (baseDN == null)
161        {
162          this.baseDN = DN.NULL_DN;
163        }
164        else
165        {
166          this.baseDN = baseDN;
167        }
168    
169    
170        // If a scope was provided, then use it.  Otherwise, use a subtree scope.
171        if (scope == null)
172        {
173          this.scope = SearchScope.SUB;
174        }
175        else
176        {
177          this.scope = scope;
178        }
179    
180    
181        // If a filter was provided, then use it.  Otherwise, use an LDAP true
182        // filter.
183        if (filter == null)
184        {
185          this.filter = Filter.createANDFilter();
186          examineFilter = false;
187        }
188        else
189        {
190          this.filter = filter;
191          if (filter.getFilterType() == Filter.FILTER_TYPE_AND)
192          {
193            examineFilter = (filter.getComponents().length > 0);
194          }
195          else
196          {
197            examineFilter = true;
198          }
199        }
200    
201    
202        examineScope =
203             (! (this.baseDN.isNullDN() && this.scope == SearchScope.SUB));
204      }
205    
206    
207    
208      /**
209       * {@inheritDoc}
210       */
211      public Entry transformEntry(final Entry e)
212      {
213        if (e == null)
214        {
215          return null;
216        }
217    
218    
219        // If we should only add the attribute to entries that don't already contain
220        // any values for that type, then determine whether the target attribute
221        // already exists in the entry.  If so, then just return the original entry.
222        if (onlyIfMissing)
223        {
224          for (final String name : names)
225          {
226            if (e.hasAttribute(name))
227            {
228              return e;
229            }
230          }
231        }
232    
233    
234        // Determine whether the entry is within the scope of the inclusion
235        // criteria.  If not, then return the original entry.
236        try
237        {
238          if (examineScope && (! e.matchesBaseAndScope(baseDN, scope)))
239          {
240            return e;
241          }
242        }
243        catch (final Exception ex)
244        {
245          // This should only happen if the entry has a malformed DN.  In that case,
246          // we'll assume it isn't within the scope and return the provided entry.
247          Debug.debugException(ex);
248          return e;
249        }
250    
251    
252        // Determine whether the entry matches the suppression filter.  If not, then
253        // return the original entry.
254        try
255        {
256          if (examineFilter && (! filter.matchesEntry(e, schema)))
257          {
258            return e;
259          }
260        }
261        catch (final Exception ex)
262        {
263          // If we can't verify whether the entry matches the filter, then assume
264          // it doesn't and return the provided entry.
265          Debug.debugException(ex);
266          return e;
267        }
268    
269    
270        // If we've gotten here, then we should add the attribute to the entry.
271        final Entry copy = e.duplicate();
272        final Attribute existingAttribute =
273             copy.getAttribute(attributeToAdd.getName(), schema);
274        if (existingAttribute == null)
275        {
276          copy.addAttribute(attributeToAdd);
277        }
278        else
279        {
280          copy.addAttribute(existingAttribute.getName(),
281               attributeToAdd.getValueByteArrays());
282        }
283        return copy;
284      }
285    
286    
287    
288      /**
289       * {@inheritDoc}
290       */
291      public Entry translate(final Entry original, final long firstLineNumber)
292      {
293        return transformEntry(original);
294      }
295    
296    
297    
298      /**
299       * {@inheritDoc}
300       */
301      public Entry translateEntryToWrite(final Entry original)
302      {
303        return transformEntry(original);
304      }
305    }