001/*
002 * Copyright 2016-2017 UnboundID Corp.
003 * All Rights Reserved.
004 */
005/*
006 * Copyright (C) 2016-2017 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 */
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 = 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}