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.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 =
115         new ArrayList<Modification>(changes.length);
116    for (final byte[] changeBytes : changes)
117    {
118      int colonPos = -1;
119      for (int i=0; i < changeBytes.length; i++)
120      {
121        if (changeBytes[i] == ':')
122        {
123          colonPos = i;
124          break;
125        }
126      }
127
128      if (colonPos < 0)
129      {
130        throw new LDAPException(ResultCode.DECODING_ERROR,
131             ERR_LOGSCHEMA_DECODE_MODIFY_CHANGE_MISSING_COLON.get(entry.getDN(),
132                  ATTR_ATTRIBUTE_CHANGES,
133                  StaticUtils.toUTF8String(changeBytes)));
134      }
135      else if (colonPos == 0)
136      {
137        throw new LDAPException(ResultCode.DECODING_ERROR,
138             ERR_LOGSCHEMA_DECODE_MODIFY_CHANGE_MISSING_ATTR.get(entry.getDN(),
139                  ATTR_ATTRIBUTE_CHANGES,
140                  StaticUtils.toUTF8String(changeBytes)));
141      }
142
143      final String attrName =
144           StaticUtils.toUTF8String(changeBytes, 0, colonPos);
145
146      if (colonPos == (changeBytes.length - 1))
147      {
148        throw new LDAPException(ResultCode.DECODING_ERROR,
149             ERR_LOGSCHEMA_DECODE_MODIFY_CHANGE_MISSING_CHANGE_TYPE.get(
150                  entry.getDN(), ATTR_ATTRIBUTE_CHANGES,
151                  StaticUtils.toUTF8String(changeBytes)));
152      }
153
154      final boolean needValue;
155      final ModificationType modType;
156      switch (changeBytes[colonPos+1])
157      {
158        case '+':
159          modType = ModificationType.ADD;
160          needValue = true;
161          break;
162        case '-':
163          modType = ModificationType.DELETE;
164          needValue = false;
165          break;
166        case '=':
167          modType = ModificationType.REPLACE;
168          needValue = false;
169          break;
170        case '#':
171          modType = ModificationType.INCREMENT;
172          needValue = true;
173          break;
174        default:
175          throw new LDAPException(ResultCode.DECODING_ERROR,
176               ERR_LOGSCHEMA_DECODE_MODIFY_CHANGE_INVALID_CHANGE_TYPE.get(
177                    entry.getDN(), ATTR_ATTRIBUTE_CHANGES,
178                    StaticUtils.toUTF8String(changeBytes)));
179      }
180
181      if (changeBytes.length == (colonPos+2))
182      {
183        if (needValue)
184        {
185          throw new LDAPException(ResultCode.DECODING_ERROR,
186               ERR_LOGSCHEMA_DECODE_MODIFY_CHANGE_MISSING_VALUE.get(
187                    entry.getDN(), ATTR_ATTRIBUTE_CHANGES,
188                    StaticUtils.toUTF8String(changeBytes),
189                    modType.getName()));
190        }
191        else
192        {
193          mods.add(new Modification(modType, attrName));
194          continue;
195        }
196      }
197
198      if ((changeBytes.length == (colonPos+3)) ||
199          (changeBytes[colonPos+2] != ' '))
200      {
201        throw new LDAPException(ResultCode.DECODING_ERROR,
202             ERR_LOGSCHEMA_DECODE_MODIFY_CHANGE_MISSING_SPACE.get(
203                  entry.getDN(), ATTR_ATTRIBUTE_CHANGES,
204                  StaticUtils.toUTF8String(changeBytes),
205                  modType.getName()));
206      }
207
208      final byte[] attrValue = new byte[changeBytes.length - colonPos - 3];
209      if (attrValue.length > 0)
210      {
211        System.arraycopy(changeBytes, (colonPos+3), attrValue, 0,
212             attrValue.length);
213      }
214
215      if (mods.isEmpty())
216      {
217        mods.add(new Modification(modType, attrName, attrValue));
218        continue;
219      }
220
221      final Modification lastMod = mods.get(mods.size() - 1);
222      if ((lastMod.getModificationType() == modType) &&
223          (lastMod.getAttributeName().equalsIgnoreCase(attrName)))
224      {
225        final byte[][] lastModValues = lastMod.getValueByteArrays();
226        final byte[][] newValues = new byte[lastModValues.length+1][];
227        System.arraycopy(lastModValues, 0, newValues, 0, lastModValues.length);
228        newValues[lastModValues.length] = attrValue;
229        mods.set((mods.size()-1),
230             new Modification(modType, lastMod.getAttributeName(), newValues));
231      }
232      else
233      {
234        mods.add(new Modification(modType, attrName, attrValue));
235      }
236    }
237
238    modifications = Collections.unmodifiableList(mods);
239
240
241    // Get the former attribute values, if present.
242    final byte[][] formerAttrBytes =
243         entry.getAttributeValueByteArrays(ATTR_FORMER_ATTRIBUTE);
244    if ((formerAttrBytes == null) || (formerAttrBytes.length == 0))
245    {
246      formerAttributes = Collections.emptyList();
247      return;
248    }
249
250    final LinkedHashMap<String,List<Attribute>> attrMap =
251         new LinkedHashMap<String,List<Attribute>>(formerAttrBytes.length);
252    for (final byte[] attrBytes : formerAttrBytes)
253    {
254      int colonPos = -1;
255      for (int i=0; i < attrBytes.length; i++)
256      {
257        if (attrBytes[i] == ':')
258        {
259          colonPos = i;
260          break;
261        }
262      }
263
264      if (colonPos < 0)
265      {
266        throw new LDAPException(ResultCode.DECODING_ERROR,
267             ERR_LOGSCHEMA_DECODE_MODIFY_OLD_ATTR_MISSING_COLON.get(
268                  entry.getDN(), ATTR_FORMER_ATTRIBUTE,
269                  StaticUtils.toUTF8String(attrBytes)));
270      }
271      else if (colonPos == 0)
272      {
273        throw new LDAPException(ResultCode.DECODING_ERROR,
274             ERR_LOGSCHEMA_DECODE_MODIFY_OLD_ATTR_MISSING_ATTR.get(
275                  entry.getDN(), ATTR_FORMER_ATTRIBUTE,
276                  StaticUtils.toUTF8String(attrBytes)));
277      }
278
279      if ((colonPos == (attrBytes.length - 1)) ||
280          (attrBytes[colonPos+1] != ' '))
281      {
282        throw new LDAPException(ResultCode.DECODING_ERROR,
283             ERR_LOGSCHEMA_DECODE_MODIFY_OLD_ATTR_MISSING_SPACE.get(
284                  entry.getDN(), ATTR_FORMER_ATTRIBUTE,
285                  StaticUtils.toUTF8String(attrBytes)));
286      }
287
288      final String attrName =
289           StaticUtils.toUTF8String(attrBytes, 0, colonPos);
290      final String lowerName = StaticUtils.toLowerCase(attrName);
291
292      List<Attribute> attrList = attrMap.get(lowerName);
293      if (attrList == null)
294      {
295        attrList = new ArrayList<Attribute>(10);
296        attrMap.put(lowerName, attrList);
297      }
298
299      final byte[] attrValue = new byte[attrBytes.length - colonPos - 2];
300      if (attrValue.length > 0)
301      {
302        System.arraycopy(attrBytes, colonPos + 2, attrValue, 0,
303             attrValue.length);
304      }
305
306      attrList.add(new Attribute(attrName, attrValue));
307    }
308
309    final ArrayList<Attribute> oldAttributes =
310         new ArrayList<Attribute>(attrMap.size());
311    for (final List<Attribute> attrList : attrMap.values())
312    {
313      if (attrList.size() == 1)
314      {
315        oldAttributes.addAll(attrList);
316      }
317      else
318      {
319        final byte[][] valueArray = new byte[attrList.size()][];
320        for (int i=0; i < attrList.size(); i++)
321        {
322          valueArray[i] = attrList.get(i).getValueByteArray();
323        }
324        oldAttributes.add(new Attribute(attrList.get(0).getName(), valueArray));
325      }
326    }
327
328    formerAttributes = Collections.unmodifiableList(oldAttributes);
329  }
330
331
332
333  /**
334   * Retrieves the modifications for the modify request described by this modify
335   * access log entry.
336   *
337   * @return  The modifications for the modify request described by this modify
338   *          access log entry.
339   */
340   public List<Modification> getModifications()
341   {
342     return modifications;
343   }
344
345
346
347  /**
348   * Retrieves a list of former versions of modified attributes described by
349   * this modify access log entry, if available.
350   *
351   * @return  A list of former versions of modified attributes, or an empty list
352   *          if no former attribute information was included in the access log
353   *          entry.
354   */
355  public List<Attribute> getFormerAttributes()
356  {
357    return formerAttributes;
358  }
359
360
361
362  /**
363   * Retrieves a {@code ModifyRequest} created from this modify access log
364   * entry.
365   *
366   * @return  The {@code ModifyRequest} created from this modify access log
367   *          entry.
368   */
369  public ModifyRequest toModifyRequest()
370  {
371    return new ModifyRequest(getTargetEntryDN(), modifications,
372         getRequestControlArray());
373  }
374}