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