001/*
002 * Copyright 2016-2019 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright (C) 2016-2019 Ping Identity Corporation
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 */
021package com.unboundid.ldap.sdk.transformations;
022
023
024
025import java.util.Collections;
026import java.util.HashSet;
027import java.util.Set;
028
029import com.unboundid.ldap.sdk.Attribute;
030import com.unboundid.ldap.sdk.DN;
031import com.unboundid.ldap.sdk.Entry;
032import com.unboundid.ldap.sdk.Filter;
033import com.unboundid.ldap.sdk.SearchScope;
034import com.unboundid.ldap.sdk.schema.AttributeTypeDefinition;
035import com.unboundid.ldap.sdk.schema.Schema;
036import com.unboundid.util.Debug;
037import com.unboundid.util.StaticUtils;
038import com.unboundid.util.ThreadSafety;
039import 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)
049public 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 =
141         new HashSet<>(StaticUtils.computeMapCapacity(5));
142    final String baseName =
143         StaticUtils.toLowerCase(attributeToAdd.getBaseName());
144    attrNames.add(baseName);
145    if (s != null)
146    {
147      final AttributeTypeDefinition at = s.getAttributeType(baseName);
148      if (at != null)
149      {
150        attrNames.add(StaticUtils.toLowerCase(at.getOID()));
151        for (final String name : at.getNames())
152        {
153          attrNames.add(StaticUtils.toLowerCase(name));
154        }
155      }
156    }
157    names = Collections.unmodifiableSet(attrNames);
158
159
160    // If a base DN was provided, then use it.  Otherwise, use the null DN.
161    if (baseDN == null)
162    {
163      this.baseDN = DN.NULL_DN;
164    }
165    else
166    {
167      this.baseDN = baseDN;
168    }
169
170
171    // If a scope was provided, then use it.  Otherwise, use a subtree scope.
172    if (scope == null)
173    {
174      this.scope = SearchScope.SUB;
175    }
176    else
177    {
178      this.scope = scope;
179    }
180
181
182    // If a filter was provided, then use it.  Otherwise, use an LDAP true
183    // filter.
184    if (filter == null)
185    {
186      this.filter = Filter.createANDFilter();
187      examineFilter = false;
188    }
189    else
190    {
191      this.filter = filter;
192      if (filter.getFilterType() == Filter.FILTER_TYPE_AND)
193      {
194        examineFilter = (filter.getComponents().length > 0);
195      }
196      else
197      {
198        examineFilter = true;
199      }
200    }
201
202
203    examineScope =
204         (! (this.baseDN.isNullDN() && this.scope == SearchScope.SUB));
205  }
206
207
208
209  /**
210   * {@inheritDoc}
211   */
212  @Override()
213  public Entry transformEntry(final Entry e)
214  {
215    if (e == null)
216    {
217      return null;
218    }
219
220
221    // If we should only add the attribute to entries that don't already contain
222    // any values for that type, then determine whether the target attribute
223    // already exists in the entry.  If so, then just return the original entry.
224    if (onlyIfMissing)
225    {
226      for (final String name : names)
227      {
228        if (e.hasAttribute(name))
229        {
230          return e;
231        }
232      }
233    }
234
235
236    // Determine whether the entry is within the scope of the inclusion
237    // criteria.  If not, then return the original entry.
238    try
239    {
240      if (examineScope && (! e.matchesBaseAndScope(baseDN, scope)))
241      {
242        return e;
243      }
244    }
245    catch (final Exception ex)
246    {
247      // This should only happen if the entry has a malformed DN.  In that case,
248      // we'll assume it isn't within the scope and return the provided entry.
249      Debug.debugException(ex);
250      return e;
251    }
252
253
254    // Determine whether the entry matches the suppression filter.  If not, then
255    // return the original entry.
256    try
257    {
258      if (examineFilter && (! filter.matchesEntry(e, schema)))
259      {
260        return e;
261      }
262    }
263    catch (final Exception ex)
264    {
265      // If we can't verify whether the entry matches the filter, then assume
266      // it doesn't and return the provided entry.
267      Debug.debugException(ex);
268      return e;
269    }
270
271
272    // If we've gotten here, then we should add the attribute to the entry.
273    final Entry copy = e.duplicate();
274    final Attribute existingAttribute =
275         copy.getAttribute(attributeToAdd.getName(), schema);
276    if (existingAttribute == null)
277    {
278      copy.addAttribute(attributeToAdd);
279    }
280    else
281    {
282      copy.addAttribute(existingAttribute.getName(),
283           attributeToAdd.getValueByteArrays());
284    }
285    return copy;
286  }
287
288
289
290  /**
291   * {@inheritDoc}
292   */
293  @Override()
294  public Entry translate(final Entry original, final long firstLineNumber)
295  {
296    return transformEntry(original);
297  }
298
299
300
301  /**
302   * {@inheritDoc}
303   */
304  @Override()
305  public Entry translateEntryToWrite(final Entry original)
306  {
307    return transformEntry(original);
308  }
309}