package com.hwlcn.ldap.ldap.sdk; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.StringTokenizer; import com.hwlcn.ldap.ldif.LDIFAddChangeRecord; import com.hwlcn.ldap.ldif.LDIFChangeRecord; import com.hwlcn.ldap.ldif.LDIFDeleteChangeRecord; import com.hwlcn.ldap.ldif.LDIFException; import com.hwlcn.ldap.ldif.LDIFModifyChangeRecord; import com.hwlcn.ldap.ldif.LDIFModifyDNChangeRecord; import com.hwlcn.ldap.ldif.LDIFReader; import com.hwlcn.ldap.ldap.matchingrules.BooleanMatchingRule; import com.hwlcn.ldap.ldap.matchingrules.DistinguishedNameMatchingRule; import com.hwlcn.ldap.ldap.matchingrules.IntegerMatchingRule; import com.hwlcn.ldap.ldap.matchingrules.OctetStringMatchingRule; import com.hwlcn.ldap.util.Debug; import com.hwlcn.core.annotation.NotExtensible; import com.hwlcn.core.annotation.NotMutable; import com.hwlcn.core.annotation.ThreadSafety; import com.hwlcn.ldap.util.ThreadSafetyLevel; import static com.hwlcn.ldap.ldap.sdk.LDAPMessages.*; import static com.hwlcn.ldap.util.StaticUtils.*; @NotExtensible() @NotMutable() @ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE) public class ChangeLogEntry extends ReadOnlyEntry { public static final String ATTR_CHANGE_NUMBER = "changeNumber"; public static final String ATTR_TARGET_DN = "targetDN"; public static final String ATTR_CHANGE_TYPE = "changeType"; public static final String ATTR_CHANGES = "changes"; public static final String ATTR_NEW_RDN = "newRDN"; public static final String ATTR_DELETE_OLD_RDN = "deleteOldRDN"; public static final String ATTR_NEW_SUPERIOR = "newSuperior"; public static final String ATTR_DELETED_ENTRY_ATTRS = "deletedEntryAttrs"; private static final long serialVersionUID = -4018129098468341663L; private final boolean deleteOldRDN; private final ChangeType changeType; private final List<Attribute> attributes; private final List<Modification> modifications; private final long changeNumber; private final String newRDN; private final String newSuperior; private final String targetDN; public ChangeLogEntry(final Entry entry) throws LDAPException { super(entry); final Attribute changeNumberAttr = entry.getAttribute(ATTR_CHANGE_NUMBER); if ((changeNumberAttr == null) || (! changeNumberAttr.hasValue())) { throw new LDAPException(ResultCode.DECODING_ERROR, ERR_CHANGELOG_NO_CHANGE_NUMBER.get()); } try { changeNumber = Long.parseLong(changeNumberAttr.getValue()); } catch (NumberFormatException nfe) { Debug.debugException(nfe); throw new LDAPException(ResultCode.DECODING_ERROR, ERR_CHANGELOG_INVALID_CHANGE_NUMBER.get(changeNumberAttr.getValue()), nfe); } final Attribute targetDNAttr = entry.getAttribute(ATTR_TARGET_DN); if ((targetDNAttr == null) || (! targetDNAttr.hasValue())) { throw new LDAPException(ResultCode.DECODING_ERROR, ERR_CHANGELOG_NO_TARGET_DN.get()); } targetDN = targetDNAttr.getValue(); final Attribute changeTypeAttr = entry.getAttribute(ATTR_CHANGE_TYPE); if ((changeTypeAttr == null) || (! changeTypeAttr.hasValue())) { throw new LDAPException(ResultCode.DECODING_ERROR, ERR_CHANGELOG_NO_CHANGE_TYPE.get()); } changeType = ChangeType.forName(changeTypeAttr.getValue()); if (changeType == null) { throw new LDAPException(ResultCode.DECODING_ERROR, ERR_CHANGELOG_INVALID_CHANGE_TYPE.get(changeTypeAttr.getValue())); } switch (changeType) { case ADD: attributes = parseAddAttributeList(entry, ATTR_CHANGES, targetDN); modifications = null; newRDN = null; deleteOldRDN = false; newSuperior = null; break; case DELETE: attributes = parseDeletedAttributeList(entry, targetDN); modifications = null; newRDN = null; deleteOldRDN = false; newSuperior = null; break; case MODIFY: attributes = null; modifications = parseModificationList(entry, targetDN); newRDN = null; deleteOldRDN = false; newSuperior = null; break; case MODIFY_DN: attributes = null; modifications = parseModificationList(entry, targetDN); newSuperior = getAttributeValue(ATTR_NEW_SUPERIOR); final Attribute newRDNAttr = getAttribute(ATTR_NEW_RDN); if ((newRDNAttr == null) || (! newRDNAttr.hasValue())) { throw new LDAPException(ResultCode.DECODING_ERROR, ERR_CHANGELOG_MISSING_NEW_RDN.get()); } newRDN = newRDNAttr.getValue(); final Attribute deleteOldRDNAttr = getAttribute(ATTR_DELETE_OLD_RDN); if ((deleteOldRDNAttr == null) || (! deleteOldRDNAttr.hasValue())) { throw new LDAPException(ResultCode.DECODING_ERROR, ERR_CHANGELOG_MISSING_DELETE_OLD_RDN.get()); } final String delOldRDNStr = toLowerCase(deleteOldRDNAttr.getValue()); if (delOldRDNStr.equals("true")) { deleteOldRDN = true; } else if (delOldRDNStr.equals("false")) { deleteOldRDN = false; } else { throw new LDAPException(ResultCode.DECODING_ERROR, ERR_CHANGELOG_MISSING_DELETE_OLD_RDN.get(delOldRDNStr)); } break; default: throw new LDAPException(ResultCode.DECODING_ERROR, ERR_CHANGELOG_INVALID_CHANGE_TYPE.get(changeTypeAttr.getValue())); } } public static ChangeLogEntry constructChangeLogEntry(final long changeNumber, final LDIFChangeRecord changeRecord) throws LDAPException { final Entry e = new Entry(ATTR_CHANGE_NUMBER + '=' + changeNumber + ",cn=changelog"); e.addAttribute("objectClass", "top", "changeLogEntry"); e.addAttribute(new Attribute(ATTR_CHANGE_NUMBER, IntegerMatchingRule.getInstance(), String.valueOf(changeNumber))); e.addAttribute(new Attribute(ATTR_TARGET_DN, DistinguishedNameMatchingRule.getInstance(), changeRecord.getDN())); e.addAttribute(ATTR_CHANGE_TYPE, changeRecord.getChangeType().getName()); switch (changeRecord.getChangeType()) { case ADD: final LDIFAddChangeRecord addRecord = (LDIFAddChangeRecord) changeRecord; final Entry addEntry = new Entry(addRecord.getDN(), addRecord.getAttributes()); final String[] entryLdifLines = addEntry.toLDIF(0); final StringBuilder entryLDIFBuffer = new StringBuilder(); for (int i=1; i < entryLdifLines.length; i++) { entryLDIFBuffer.append(entryLdifLines[i]); entryLDIFBuffer.append(EOL); } e.addAttribute(new Attribute(ATTR_CHANGES, OctetStringMatchingRule.getInstance(), entryLDIFBuffer.toString())); break; case DELETE: break; case MODIFY: final String[] modLdifLines = changeRecord.toLDIF(0); final StringBuilder modLDIFBuffer = new StringBuilder(); for (int i=2; i < modLdifLines.length; i++) { modLDIFBuffer.append(modLdifLines[i]); modLDIFBuffer.append(EOL); } e.addAttribute(new Attribute(ATTR_CHANGES, OctetStringMatchingRule.getInstance(), modLDIFBuffer.toString())); break; case MODIFY_DN: final LDIFModifyDNChangeRecord modDNRecord = (LDIFModifyDNChangeRecord) changeRecord; e.addAttribute(new Attribute(ATTR_NEW_RDN, DistinguishedNameMatchingRule.getInstance(), modDNRecord.getNewRDN())); e.addAttribute(new Attribute(ATTR_DELETE_OLD_RDN, BooleanMatchingRule.getInstance(), (modDNRecord.deleteOldRDN() ? "TRUE" : "FALSE"))); if (modDNRecord.getNewSuperiorDN() != null) { e.addAttribute(new Attribute(ATTR_NEW_SUPERIOR, DistinguishedNameMatchingRule.getInstance(), modDNRecord.getNewSuperiorDN())); } break; } return new ChangeLogEntry(e); } protected static List<Attribute> parseAddAttributeList(final Entry entry, final String attrName, final String targetDN) throws LDAPException { final Attribute changesAttr = entry.getAttribute(attrName); if ((changesAttr == null) || (! changesAttr.hasValue())) { throw new LDAPException(ResultCode.DECODING_ERROR, ERR_CHANGELOG_MISSING_CHANGES.get()); } final ArrayList<String> ldifLines = new ArrayList<String>(); ldifLines.add("dn: " + targetDN); final StringTokenizer tokenizer = new StringTokenizer(changesAttr.getValue(), "\r\n"); while (tokenizer.hasMoreTokens()) { ldifLines.add(tokenizer.nextToken()); } final String[] lineArray = new String[ldifLines.size()]; ldifLines.toArray(lineArray); try { final Entry e = LDIFReader.decodeEntry(lineArray); return Collections.unmodifiableList( new ArrayList<Attribute>(e.getAttributes())); } catch (LDIFException le) { Debug.debugException(le); throw new LDAPException(ResultCode.DECODING_ERROR, ERR_CHANGELOG_CANNOT_PARSE_ATTR_LIST.get(attrName, getExceptionMessage(le)), le); } } private static List<Attribute> parseDeletedAttributeList(final Entry entry, final String targetDN) throws LDAPException { final Attribute deletedEntryAttrs = entry.getAttribute(ATTR_DELETED_ENTRY_ATTRS); if ((deletedEntryAttrs == null) || (! deletedEntryAttrs.hasValue())) { return null; } final byte[] valueBytes = deletedEntryAttrs.getValueByteArray(); if ((valueBytes.length > 0) && (valueBytes[valueBytes.length-1] == 0x00)) { final String valueStr = new String(valueBytes, 0, valueBytes.length-2); final ArrayList<String> ldifLines = new ArrayList<String>(); ldifLines.add("dn: " + targetDN); ldifLines.add("changetype: modify"); final StringTokenizer tokenizer = new StringTokenizer(valueStr, "\r\n"); while (tokenizer.hasMoreTokens()) { ldifLines.add(tokenizer.nextToken()); } final String[] lineArray = new String[ldifLines.size()]; ldifLines.toArray(lineArray); try { final LDIFModifyChangeRecord changeRecord = (LDIFModifyChangeRecord) LDIFReader.decodeChangeRecord(lineArray); final Modification[] mods = changeRecord.getModifications(); final ArrayList<Attribute> attrs = new ArrayList<Attribute>(mods.length); for (final Modification m : mods) { if (! m.getModificationType().equals(ModificationType.DELETE)) { throw new LDAPException(ResultCode.DECODING_ERROR, ERR_CHANGELOG_INVALID_DELENTRYATTRS_MOD_TYPE.get( ATTR_DELETED_ENTRY_ATTRS)); } attrs.add(m.getAttribute()); } return Collections.unmodifiableList(attrs); } catch (LDIFException le) { Debug.debugException(le); throw new LDAPException(ResultCode.DECODING_ERROR, ERR_CHANGELOG_INVALID_DELENTRYATTRS_MODS.get( ATTR_DELETED_ENTRY_ATTRS, getExceptionMessage(le)), le); } } else { final ArrayList<String> ldifLines = new ArrayList<String>(); ldifLines.add("dn: " + targetDN); final StringTokenizer tokenizer = new StringTokenizer(deletedEntryAttrs.getValue(), "\r\n"); while (tokenizer.hasMoreTokens()) { ldifLines.add(tokenizer.nextToken()); } final String[] lineArray = new String[ldifLines.size()]; ldifLines.toArray(lineArray); try { final Entry e = LDIFReader.decodeEntry(lineArray); return Collections.unmodifiableList( new ArrayList<Attribute>(e.getAttributes())); } catch (LDIFException le) { Debug.debugException(le); throw new LDAPException(ResultCode.DECODING_ERROR, ERR_CHANGELOG_CANNOT_PARSE_DELENTRYATTRS.get( ATTR_DELETED_ENTRY_ATTRS, getExceptionMessage(le)), le); } } } private static List<Modification> parseModificationList(final Entry entry, final String targetDN) throws LDAPException { final Attribute changesAttr = entry.getAttribute(ATTR_CHANGES); if ((changesAttr == null) || (! changesAttr.hasValue())) { return null; } final byte[] valueBytes = changesAttr.getValueByteArray(); if (valueBytes.length == 0) { return null; } final ArrayList<String> ldifLines = new ArrayList<String>(); ldifLines.add("dn: " + targetDN); ldifLines.add("changetype: modify"); final StringTokenizer tokenizer; if ((valueBytes.length > 0) && (valueBytes[valueBytes.length-1] == 0x00)) { final String fullValue = changesAttr.getValue(); final String realValue = fullValue.substring(0, fullValue.length()-2); tokenizer = new StringTokenizer(realValue, "\r\n"); } else { tokenizer = new StringTokenizer(changesAttr.getValue(), "\r\n"); } while (tokenizer.hasMoreTokens()) { ldifLines.add(tokenizer.nextToken()); } final String[] lineArray = new String[ldifLines.size()]; ldifLines.toArray(lineArray); try { final LDIFModifyChangeRecord changeRecord = (LDIFModifyChangeRecord) LDIFReader.decodeChangeRecord(lineArray); return Collections.unmodifiableList( Arrays.asList(changeRecord.getModifications())); } catch (LDIFException le) { Debug.debugException(le); throw new LDAPException(ResultCode.DECODING_ERROR, ERR_CHANGELOG_CANNOT_PARSE_MOD_LIST.get(ATTR_CHANGES, getExceptionMessage(le)), le); } } public final long getChangeNumber() { return changeNumber; } public final String getTargetDN() { return targetDN; } public final ChangeType getChangeType() { return changeType; } public final List<Attribute> getAddAttributes() { if (changeType == ChangeType.ADD) { return attributes; } else { return null; } } public final List<Attribute> getDeletedEntryAttributes() { if (changeType == ChangeType.DELETE) { return attributes; } else { return null; } } public final List<Modification> getModifications() { return modifications; } public final String getNewRDN() { return newRDN; } public final boolean deleteOldRDN() { return deleteOldRDN; } public final String getNewSuperior() { return newSuperior; } public final String getNewDN() { switch (changeType) { case ADD: case MODIFY: return targetDN; case MODIFY_DN: break; case DELETE: default: return null; } try { final RDN parsedNewRDN = new RDN(newRDN); if (newSuperior == null) { final DN parsedTargetDN = new DN(targetDN); final DN parentDN = parsedTargetDN.getParent(); if (parentDN == null) { return new DN(parsedNewRDN).toString(); } else { return new DN(parsedNewRDN, parentDN).toString(); } } else { final DN parsedNewSuperior = new DN(newSuperior); return new DN(parsedNewRDN, parsedNewSuperior).toString(); } } catch (final Exception e) { Debug.debugException(e); return null; } } public final LDIFChangeRecord toLDIFChangeRecord() { switch (changeType) { case ADD: return new LDIFAddChangeRecord(targetDN, attributes); case DELETE: return new LDIFDeleteChangeRecord(targetDN); case MODIFY: return new LDIFModifyChangeRecord(targetDN, modifications); case MODIFY_DN: return new LDIFModifyDNChangeRecord(targetDN, newRDN, deleteOldRDN, newSuperior); default: return null; } } public final LDAPResult processChange(final LDAPInterface connection) throws LDAPException { switch (changeType) { case ADD: return connection.add(targetDN, attributes); case DELETE: return connection.delete(targetDN); case MODIFY: return connection.modify(targetDN, modifications); case MODIFY_DN: return connection.modifyDN(targetDN, newRDN, deleteOldRDN, newSuperior); default: return null; } } }