syncDoneValue ::= SEQUENCE {
cookie OCTET STRING OPTIONAL,
refreshDeletes BOOLEAN DEFAULT FALSE
}
Read this as: the value is a sequence that contains 2 other values. The first, named
cookie
is optional and has binary content. The second is named refreshDeletes
and has boolean content. The default of refreshDeletes is false. See RFC4533 for the semantics.
Lets map this to java. All controls must have the same constructor signature so that JLDAP can instantiate it. We'll start with:public class SyncDoneControl extends LDAPControl {
public static final String OID = "1.3.6.1.4.1.4203.1.9.1.3";
private byte[] cookie;
private boolean refreshDeletes;
// add getters for cookie and refresDeletes
public SyncDoneControl(String oid, boolean critical, byte[] value) {
super(oid, critical, value);
...see below
}
}
value
contains the BER encoded value of the control. The LDAP restriction put on the BER encoding mean that optional values and values that are equal to the default value must be omitted. With other words: when there is no cookie (allowed because it is declared OPTIONAL), and refreshDeletes is FALSE (which is the default), constructor argument value
is null! Just to be robust we'll check for the empty array as well: if (value == null || value.lenght == 0) {
cookie = null;
refreshDeletes = false;
} else {
...see below
}
ASN1Sequence
:ASN1Sequence asn1 = (ASN1Sequence) new LBERDecoder().decode(value);
ASN1Object#getIdentifier()
method.
We can examine the sequence further by calling the ASN1Sequence#size()
and ASN1Sequence#get(int)
methods. Again, we must take into account that each element may be omitted. You can do this by examining the type of ASN.1 value you get out of the sequence. First, extract a value from the sequence:
ASN1Object asn1Obj = asn1.get(0);
boolean isCookie =
asn1Obj.getIdentifier().getASN1Class() == ASN1Identifier.UNIVERSAL &&
asn1Obj.getIdentifier().getTag() == ASN1OctetString.TAG;
cookie = ((ASN1OctetString) asn1Obj).byteValue()
ASN1Sequence asn1 = (ASN1Sequence) new LBERDecoder().decode(value);
for (int i = 0; i < asn1.size(); i++) {
ASN1Object asnSeqObj = asn1.get(i);
if (i == 0 && Asn1Util.isOctetString(asnSeqObj)) {
cookie = Asn1Util.getByteValue(asnSeqObj);
} else if (i == (cookie == null ? 0 : 1) && Asn1Util.isBoolean(asnSeqObj)) {
refreshDeletes = Asn1Util.getBooleanValue(asnSeqObj);
} else {
throw new IllegalArgumentException("Parse error at index " + i + ", parsing: " + asnSeqObj);
}
}
LDAPControl.register(SyncDoneControl.OID, SyncDoneControl.class);
syncRequestValue ::= SEQUENCE {
mode ENUMERATED {
refreshOnly (1),
refreshAndPersist (3)
},
cookie OCTET STRING OPTIONAL,
reloadHint BOOLEAN DEFAULT FALSE
}
public enum SyncRequestMode { REFRESH_ONLY, REFRESH_AND_PERSIST }
public class SyncRequestControl extends LDAPControl {
public static final String OID = "1.3.6.1.4.1.4203.1.9.1.1";
private SyncRequestMode mode;
private byte cookie[];
boolean reloadHint = false;
public SyncRequestControl(SyncRequestMode mode, byte cookie[], boolean reloadHint) {
super(OID, true, null);
this.mode = mode;
this.cookie = cookie;
this.reloadHint = reloadHint;
setValue(encodedValue());
}
private byte[] encodedValue() throws IOException {
ASN1Sequence asn1 = new ASN1Sequence();
asn1.add(new ASN1Enumerated(mode == REFRESH_ONLY ? 1 : 3));
if (cookie != null) {
asn1.add(new ASN1OctetString(cookie));
}
if (reloadHint) {
asn1.add(new ASN1Boolean(reloadHint));
}
ByteArrayOutputStream baos = new ByteArrayOutputStream();
new LBEREncoder().encode(asn1, baos);
return baos.toByteArray();
}
syncInfoValue ::= CHOICE {
newcookie [0] OCTET STRING,
refreshDelete [1] SEQUENCE {
cookie OCTET STRING OPTIONAL,
refreshDone BOOLEAN DEFAULT TRUE
},
refreshPresent [2] SEQUENCE {
cookie OCTET STRING OPTIONAL,
refreshDone BOOLEAN DEFAULT TRUE
},
syncIdSet [3] SEQUENCE {
cookie OCTET STRING OPTIONAL,
refreshDeletes BOOLEAN DEFAULT FALSE,
syncUUIDs SET OF OCTET STRING (SIZE)16))
}
}
The ASN.1 defines that the value can have one or four values. We'll represent the chosen value with a Java enumeration. By defining the enum values in order, we can abuse that the ordinal value of the enum values corresponds to the tag (defined between brackets []) value.
public static enum SyncInfoMessageChoiceType {
// Note: order is important
NEW_COOKIE, REFRESH_DELETE, REFRESH_PRESENT, SYNC_ID_SET
}
public class SyncInfoMessage extends LDAPIntermediateResponse {
public static final String OID = "1.3.6.1.4.1.4203.1.9.1.4";
private SyncInfoMessageChoiceType syncInfoMessageChoiceType;
private byte[] cookie;
private Boolean refreshDone;
private Boolean refreshDeletes;
private List syncUuids;
// add getters ...
Not all fields will always get a value. For example field syncUuids will only be set when
syncInfoMessageChoiceType == SYNC_ID_SET
. This is the most simple implementation, and the user of this class must know about the CHOICE type anyway.
Intermediate messages must always have the same constructor, so that JLDAP can construct it for us:public SyncInfoMessage(RfcLDAPMessage message) {
super(message);
...
ASN1Tagged asn1Choice = (ASN1Tagged) new LBERDecoder().decode(getValue());
int tag = asn1Choice.getIdentifier().getTag();
syncInfoMessageChoiceType = SyncInfoMessageChoiceType.values()[tag];
asn1Choice
as an OCTET STRING, get its byte array, and decode that again with the JLDAP decoder.
So for most choice types we need to decode the tag's contents to a SEQUENCE. Here is a utility method we can add to the AsnUtil class:public static ASN1Sequence parseContentAsSequence(ASN1Tagged asn1Choice) throws IOException {
ASN1OctetString taggedValue = (ASN1OctetString) asn1Choice.taggedValue();
byte[] taggedContent = taggedValue.byteValue();
return new ASN1Sequence(new LBERDecoder(), new ByteArrayInputStream(taggedContent), taggedContent.length);
}
if (syncInfoMessageChoiceType == SyncInfoMessageChoiceType.REFRESH_PRESENT) {
ASN1Sequence asn1Seq = Asn1Util.parseContentAsSequence(asn1Choice);
for (int i = 0; i < asn1Seq.size(); i++) {
ASN1Object asnSeqObj = asn1Seq.get(i);
if (i == 0 && Asn1Util.isOctetString(asnSeqObj)) {
cookie = Asn1Util.getByteValue(asnSeqObj);
} else if ((i == (cookie == null ? 0 : 1)) && Asn1Util.isBoolean(asnSeqObj)) {
refreshDone = Asn1Util.getBooleanValue(asnSeqObj);
} else {
throw new RuntimeException("Parse error");
}
}
if (refreshDone == null) {
refreshDone = Boolean.TRUE;
}
}
asnChoice
is already an OCTET_STRING, so we can use that directly:if (syncInfoMessageChoiceType == SyncInfoMessageChoiceType.NEW_COOKIE) {
ASN1OctetString taggedValue = (ASN1OctetString) asn1Choice.taggedValue();
cookie = taggedValue.byteValue();
}
LDAPIntermediateResponse.register(SyncInfoMessage.OID, SyncInfoMessage.class);
It's a rare treat to read an article on JLDAP -- especially one about doing more than just "the basics".
ReplyDeleteOne of the main reasons I think the JLDAP library is under-utilized is because of the dearth of good articles and documentation. And the area in which this library particularly excels is in what we might call "advanced" LDAP work. But with so little information, how is the deadline-driven programmer going to know how to do this stuff? I think you've made a good contribution to rectifying the situation.
Thank you very much for this very insteresting article.
ReplyDeleteI would like to use your approach to implement a small synchronization client in order to detect when a change occures in an LDAP entry. In this context I would have two questions :
* Is it possible with another LDAP than the Novel one like OpenLDAP.
* how do you recieve the intermediate messages, with an asynchronous search, a persistant search or something else ?
Sorry if it is not the place for the questions and thanks again, your article helped me anyway.
Arnaud, the only server implementation of RFC4533 I know is done by Openldap. So that answers your first question. (Assuming you want to use RFC4533.)
ReplyDeleteRegarding your second question, within JLDAP you can receive messages in a number of ways. Novel (who donated JLDAP to Openldap) provides extensive example applications. Look up those on persistent search. If you registered your message as outlined in the article, the message you receive are instances of the registered type. In other words: you can simply do a type cast of the received message to your own message type.
Thank you for showing interest, and please ask again.
Erik, first thank you for your answer. With your explanations I think I will be able to write my client. My error was that I expected the changes to be returned as Sync Info Messages. In fact, as far as I undestand, they are returned as search results with a sync state control. When I start a sync process in a Refresh And Persist mode, a sequence of this messsages are send and then a sync info message is sent to indicate the end of the refresh stage. After that only changes are sent (still via a search result).
ReplyDelete..For short I have to read a little bit more the RFC but your tutorial is a very good starting point for my job.
Hi, this article is very informative! I still have some questions, though.
ReplyDeleteDo you use refresh only, or refresh and persist?
Do you call connection.search several times in a loop?
Where do the changed entries actually show up - as results from the search, or somewhere else (e.g. as messages)?
Thank you so much!
Dear Sandra_snan, the article does not define how to work with the protocol, just how you can use JLDAP to implement the messages. The implementation I created did both refresh and persist. More information on how the protocol works is in the standard. It is very informative!
ReplyDelete(The weird name formatting was just because I logged in with OpenID.)
ReplyDeleteThe standard just says "The server, much like it would with a normal search operation, returns (subject to access controls and other restrictions) the content matching the search criteria (baseObject, scope, filter, attributes)."
How to access that content seem to vary between LDAP api implementations. I realize that this has become off-topic for this article so I'll keep looking elsewhere. Thanks anyway.
Sandra, its been a long time since I did something with LDAP. But please look at the JLDAP examples. You'll probably find something usefull there.
ReplyDelete