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}