001    /*
002     * Copyright 2016 UnboundID Corp.
003     * All Rights Reserved.
004     */
005    /*
006     * Copyright (C) 2016 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     */
021    package com.unboundid.ldap.sdk.experimental;
022    
023    
024    
025    import java.util.ArrayList;
026    import java.util.Collections;
027    import java.util.LinkedHashMap;
028    import java.util.List;
029    
030    import com.unboundid.ldap.sdk.Attribute;
031    import com.unboundid.ldap.sdk.ModifyRequest;
032    import com.unboundid.ldap.sdk.Entry;
033    import com.unboundid.ldap.sdk.LDAPException;
034    import com.unboundid.ldap.sdk.Modification;
035    import com.unboundid.ldap.sdk.ModificationType;
036    import com.unboundid.ldap.sdk.OperationType;
037    import com.unboundid.ldap.sdk.ResultCode;
038    import com.unboundid.util.NotMutable;
039    import com.unboundid.util.StaticUtils;
040    import com.unboundid.util.ThreadSafety;
041    import com.unboundid.util.ThreadSafetyLevel;
042    
043    import 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)
054    public 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    }