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