001/*
002 * Copyright 2016-2018 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright (C) 2016-2018 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.experimental;
022
023
024
025import java.util.ArrayList;
026import java.util.Collections;
027import java.util.LinkedHashMap;
028import java.util.List;
029
030import com.unboundid.ldap.sdk.Attribute;
031import com.unboundid.ldap.sdk.ModifyRequest;
032import com.unboundid.ldap.sdk.Entry;
033import com.unboundid.ldap.sdk.LDAPException;
034import com.unboundid.ldap.sdk.Modification;
035import com.unboundid.ldap.sdk.ModificationType;
036import com.unboundid.ldap.sdk.OperationType;
037import com.unboundid.ldap.sdk.ResultCode;
038import com.unboundid.util.NotMutable;
039import com.unboundid.util.StaticUtils;
040import com.unboundid.util.ThreadSafety;
041import com.unboundid.util.ThreadSafetyLevel;
042
043import static com.unboundid.ldap.sdk.experimental.ExperimentalMessages.*;
044
045
046
047/**
048 * This class represents an entry that holds information about a modify
049 * operation processed by an LDAP server, as per the specification described in
050 * draft-chu-ldap-logschema-00.
051 */
052@NotMutable()
053@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE)
054public final class DraftChuLDAPLogSchema00ModifyEntry
055       extends DraftChuLDAPLogSchema00Entry
056{
057  /**
058   * The name of the attribute used to hold the attribute changes contained in
059   * the modify operation.
060   */
061  public static final String ATTR_ATTRIBUTE_CHANGES = "reqMod";
062
063
064
065  /**
066   * The name of the attribute used to hold the former values of entries changed
067   * by the modify operation.
068   */
069  public static final String ATTR_FORMER_ATTRIBUTE = "reqOld";
070
071
072
073  /**
074   * The serial version UID for this serializable class.
075   */
076  private static final long serialVersionUID = 5787071409404025072L;
077
078
079
080  // A list of the former versions of modified attributes.
081  private final List<Attribute> formerAttributes;
082
083  // A list of the modifications contained in the request.
084  private final List<Modification> modifications;
085
086
087
088  /**
089   * Creates a new instance of this modify access log entry from the provided
090   * entry.
091   *
092   * @param  entry  The entry used to create this modify access log entry.
093   *
094   * @throws  LDAPException  If the provided entry cannot be decoded as a valid
095   *                         modify access log entry as per the specification
096   *                         contained in draft-chu-ldap-logschema-00.
097   */
098  public DraftChuLDAPLogSchema00ModifyEntry(final Entry entry)
099         throws LDAPException
100  {
101    super(entry, OperationType.MODIFY);
102
103
104    // Process the set of modifications.
105    final byte[][] changes =
106         entry.getAttributeValueByteArrays(ATTR_ATTRIBUTE_CHANGES);
107    if ((changes == null) || (changes.length == 0))
108    {
109      throw new LDAPException(ResultCode.DECODING_ERROR,
110           ERR_LOGSCHEMA_DECODE_MISSING_REQUIRED_ATTR.get(entry.getDN(),
111                ATTR_ATTRIBUTE_CHANGES));
112    }
113
114    final ArrayList<Modification> mods = new ArrayList<>(changes.length);
115    for (final byte[] changeBytes : changes)
116    {
117      int colonPos = -1;
118      for (int i=0; i < changeBytes.length; i++)
119      {
120        if (changeBytes[i] == ':')
121        {
122          colonPos = i;
123          break;
124        }
125      }
126
127      if (colonPos < 0)
128      {
129        throw new LDAPException(ResultCode.DECODING_ERROR,
130             ERR_LOGSCHEMA_DECODE_MODIFY_CHANGE_MISSING_COLON.get(entry.getDN(),
131                  ATTR_ATTRIBUTE_CHANGES,
132                  StaticUtils.toUTF8String(changeBytes)));
133      }
134      else if (colonPos == 0)
135      {
136        throw new LDAPException(ResultCode.DECODING_ERROR,
137             ERR_LOGSCHEMA_DECODE_MODIFY_CHANGE_MISSING_ATTR.get(entry.getDN(),
138                  ATTR_ATTRIBUTE_CHANGES,
139                  StaticUtils.toUTF8String(changeBytes)));
140      }
141
142      final String attrName =
143           StaticUtils.toUTF8String(changeBytes, 0, colonPos);
144
145      if (colonPos == (changeBytes.length - 1))
146      {
147        throw new LDAPException(ResultCode.DECODING_ERROR,
148             ERR_LOGSCHEMA_DECODE_MODIFY_CHANGE_MISSING_CHANGE_TYPE.get(
149                  entry.getDN(), ATTR_ATTRIBUTE_CHANGES,
150                  StaticUtils.toUTF8String(changeBytes)));
151      }
152
153      final boolean needValue;
154      final ModificationType modType;
155      switch (changeBytes[colonPos+1])
156      {
157        case '+':
158          modType = ModificationType.ADD;
159          needValue = true;
160          break;
161        case '-':
162          modType = ModificationType.DELETE;
163          needValue = false;
164          break;
165        case '=':
166          modType = ModificationType.REPLACE;
167          needValue = false;
168          break;
169        case '#':
170          modType = ModificationType.INCREMENT;
171          needValue = true;
172          break;
173        default:
174          throw new LDAPException(ResultCode.DECODING_ERROR,
175               ERR_LOGSCHEMA_DECODE_MODIFY_CHANGE_INVALID_CHANGE_TYPE.get(
176                    entry.getDN(), ATTR_ATTRIBUTE_CHANGES,
177                    StaticUtils.toUTF8String(changeBytes)));
178      }
179
180      if (changeBytes.length == (colonPos+2))
181      {
182        if (needValue)
183        {
184          throw new LDAPException(ResultCode.DECODING_ERROR,
185               ERR_LOGSCHEMA_DECODE_MODIFY_CHANGE_MISSING_VALUE.get(
186                    entry.getDN(), ATTR_ATTRIBUTE_CHANGES,
187                    StaticUtils.toUTF8String(changeBytes),
188                    modType.getName()));
189        }
190        else
191        {
192          mods.add(new Modification(modType, attrName));
193          continue;
194        }
195      }
196
197      if ((changeBytes.length == (colonPos+3)) ||
198          (changeBytes[colonPos+2] != ' '))
199      {
200        throw new LDAPException(ResultCode.DECODING_ERROR,
201             ERR_LOGSCHEMA_DECODE_MODIFY_CHANGE_MISSING_SPACE.get(
202                  entry.getDN(), ATTR_ATTRIBUTE_CHANGES,
203                  StaticUtils.toUTF8String(changeBytes),
204                  modType.getName()));
205      }
206
207      final byte[] attrValue = new byte[changeBytes.length - colonPos - 3];
208      if (attrValue.length > 0)
209      {
210        System.arraycopy(changeBytes, (colonPos+3), attrValue, 0,
211             attrValue.length);
212      }
213
214      if (mods.isEmpty())
215      {
216        mods.add(new Modification(modType, attrName, attrValue));
217        continue;
218      }
219
220      final Modification lastMod = mods.get(mods.size() - 1);
221      if ((lastMod.getModificationType() == modType) &&
222          (lastMod.getAttributeName().equalsIgnoreCase(attrName)))
223      {
224        final byte[][] lastModValues = lastMod.getValueByteArrays();
225        final byte[][] newValues = new byte[lastModValues.length+1][];
226        System.arraycopy(lastModValues, 0, newValues, 0, lastModValues.length);
227        newValues[lastModValues.length] = attrValue;
228        mods.set((mods.size()-1),
229             new Modification(modType, lastMod.getAttributeName(), newValues));
230      }
231      else
232      {
233        mods.add(new Modification(modType, attrName, attrValue));
234      }
235    }
236
237    modifications = Collections.unmodifiableList(mods);
238
239
240    // Get the former attribute values, if present.
241    final byte[][] formerAttrBytes =
242         entry.getAttributeValueByteArrays(ATTR_FORMER_ATTRIBUTE);
243    if ((formerAttrBytes == null) || (formerAttrBytes.length == 0))
244    {
245      formerAttributes = Collections.emptyList();
246      return;
247    }
248
249    final LinkedHashMap<String,List<Attribute>> attrMap =
250         new LinkedHashMap<>(formerAttrBytes.length);
251    for (final byte[] attrBytes : formerAttrBytes)
252    {
253      int colonPos = -1;
254      for (int i=0; i < attrBytes.length; i++)
255      {
256        if (attrBytes[i] == ':')
257        {
258          colonPos = i;
259          break;
260        }
261      }
262
263      if (colonPos < 0)
264      {
265        throw new LDAPException(ResultCode.DECODING_ERROR,
266             ERR_LOGSCHEMA_DECODE_MODIFY_OLD_ATTR_MISSING_COLON.get(
267                  entry.getDN(), ATTR_FORMER_ATTRIBUTE,
268                  StaticUtils.toUTF8String(attrBytes)));
269      }
270      else if (colonPos == 0)
271      {
272        throw new LDAPException(ResultCode.DECODING_ERROR,
273             ERR_LOGSCHEMA_DECODE_MODIFY_OLD_ATTR_MISSING_ATTR.get(
274                  entry.getDN(), ATTR_FORMER_ATTRIBUTE,
275                  StaticUtils.toUTF8String(attrBytes)));
276      }
277
278      if ((colonPos == (attrBytes.length - 1)) ||
279          (attrBytes[colonPos+1] != ' '))
280      {
281        throw new LDAPException(ResultCode.DECODING_ERROR,
282             ERR_LOGSCHEMA_DECODE_MODIFY_OLD_ATTR_MISSING_SPACE.get(
283                  entry.getDN(), ATTR_FORMER_ATTRIBUTE,
284                  StaticUtils.toUTF8String(attrBytes)));
285      }
286
287      final String attrName =
288           StaticUtils.toUTF8String(attrBytes, 0, colonPos);
289      final String lowerName = StaticUtils.toLowerCase(attrName);
290
291      List<Attribute> attrList = attrMap.get(lowerName);
292      if (attrList == null)
293      {
294        attrList = new ArrayList<>(10);
295        attrMap.put(lowerName, attrList);
296      }
297
298      final byte[] attrValue = new byte[attrBytes.length - colonPos - 2];
299      if (attrValue.length > 0)
300      {
301        System.arraycopy(attrBytes, colonPos + 2, attrValue, 0,
302             attrValue.length);
303      }
304
305      attrList.add(new Attribute(attrName, attrValue));
306    }
307
308    final ArrayList<Attribute> oldAttributes = new ArrayList<>(attrMap.size());
309    for (final List<Attribute> attrList : attrMap.values())
310    {
311      if (attrList.size() == 1)
312      {
313        oldAttributes.addAll(attrList);
314      }
315      else
316      {
317        final byte[][] valueArray = new byte[attrList.size()][];
318        for (int i=0; i < attrList.size(); i++)
319        {
320          valueArray[i] = attrList.get(i).getValueByteArray();
321        }
322        oldAttributes.add(new Attribute(attrList.get(0).getName(), valueArray));
323      }
324    }
325
326    formerAttributes = Collections.unmodifiableList(oldAttributes);
327  }
328
329
330
331  /**
332   * Retrieves the modifications for the modify request described by this modify
333   * access log entry.
334   *
335   * @return  The modifications for the modify request described by this modify
336   *          access log entry.
337   */
338   public List<Modification> getModifications()
339   {
340     return modifications;
341   }
342
343
344
345  /**
346   * Retrieves a list of former versions of modified attributes described by
347   * this modify access log entry, if available.
348   *
349   * @return  A list of former versions of modified attributes, or an empty list
350   *          if no former attribute information was included in the access log
351   *          entry.
352   */
353  public List<Attribute> getFormerAttributes()
354  {
355    return formerAttributes;
356  }
357
358
359
360  /**
361   * Retrieves a {@code ModifyRequest} created from this modify access log
362   * entry.
363   *
364   * @return  The {@code ModifyRequest} created from this modify access log
365   *          entry.
366   */
367  public ModifyRequest toModifyRequest()
368  {
369    return new ModifyRequest(getTargetEntryDN(), modifications,
370         getRequestControlArray());
371  }
372}