Parses a ldap search filter and builds an AST, returns an instance of Node
.
To further evaluate the filter, one can use a NodeVisitor
and call Node::accept(NodeVisitor)
.
Created
February 26, 2021 12:52
-
-
Save mon-compte-github/c9fed93f28e14388f2015777c4a7a98f to your computer and use it in GitHub Desktop.
Ldap search filter parsing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import java.util.ArrayList; | |
import java.util.HashSet; | |
import java.util.List; | |
import java.util.Set; | |
/** | |
* Manipule un filtre de recherche au format "filtre de recherche ldap". | |
* La spéc a subit quelques modifications pour l'adapter à nos besoins | |
* <ul> | |
* <li>Pour simplifier le parsing, on ne gère ques des opérateurs sur un caractère</li> | |
* <li>Les opérateur AND et OR peuvent avoir de 2 à n opérandes</li> | |
* <li>Les variables sont toujours du côté gauche de l'opérateur de comparaison</li> | |
* </ul> | |
* | |
* @author jay | |
* | |
* @see http://msdn.microsoft.com/en-us/library/windows/desktop/aa746475(v=vs.85).aspx | |
* @see http://technet.microsoft.com/en-us/library/aa996205(v=exchg.65).aspx | |
*/ | |
public class LdapSearchFilter { | |
/* | |
* Inner classes | |
*/ | |
/** | |
* Un itérateur sur une chaîne. Permet de parcourir | |
* les caractères un par un, avec possibilité de | |
* retour arrière sur un caractère. | |
* @author jay | |
*/ | |
private static class StringIterator { | |
private int index = 0; | |
private char c = '\0'; | |
private String str; | |
public StringIterator(String str) { | |
this.str = str; | |
} | |
/** | |
* Permet de savoir s'il reste des caractères | |
* non parcourus dans la chaîne (i.e. l'appel à | |
* {@link #next()} ne provoquera pas d'exception). | |
* @return <code>true</code> si l'itérateur n'est pas | |
* arrivé à la fin de la chaîne. | |
*/ | |
public boolean hasNext() { | |
return (c != 0) || (index < str.length()); | |
} | |
public char next() { | |
if(!hasNext()) | |
throw new IllegalStateException("Fin de chaîne prématurée"); | |
if(c != 0) { | |
char temp = c; | |
c = 0; | |
return temp; | |
} | |
return str.charAt(index++); | |
} | |
public void pushBack(char c) { | |
if(this.c != 0) | |
throw new IllegalStateException("Impossible de revenir en arrière 2 fois"); | |
this.c = c; | |
} | |
} | |
/** | |
* Les opérateurs qui combinent les filtres. | |
* @author jay | |
*/ | |
public static enum CombiningOperator { | |
AND('&'), OR('|'), NOT('!'); | |
private char op; | |
private CombiningOperator(char c) { | |
this.op = c; | |
} | |
public char value() { | |
return op; | |
} | |
public static CombiningOperator fromValue(char c) { | |
for(CombiningOperator operator : values()) { | |
if(operator.op == c) return operator; | |
} | |
return null; | |
} | |
} | |
/** | |
* Les opérateurs de test de valeur. | |
* @author jay | |
*/ | |
public static enum TestingOperator { | |
// l'operateur approximatif (~=) n'est pas géré | |
// les opérateurs <= et >= sont remplacés par leurs | |
// équivalents stricts (pour tenir sur 1 caractère) | |
EQ('='), LT('<'), GT('>'); | |
private char op; | |
private TestingOperator(char c) { | |
this.op = c; | |
} | |
public char value() { | |
return op; | |
} | |
public static TestingOperator fromValue(char c) { | |
for(TestingOperator operator : values()) { | |
if(operator.op == c) return operator; | |
} | |
return null; | |
} | |
} | |
/** | |
* Définit l'interface à implémenter par un visiteur. | |
* @author jay | |
*/ | |
public static interface NodeVisitor { | |
/** | |
* La méthode appellée pour visiter les noeuds. | |
* @param node Une instance de {@link Filtre} ou de {@link Expression}. | |
*/ | |
void visit(Node node); | |
} | |
/** | |
* Classe de base de tous les noeuds. | |
* @author jay | |
*/ | |
private static abstract class Node { | |
/** | |
* Génère une représentation textuelle du noeud | |
* et l'ajoute au buffer passé en paramètre. | |
* @param sb Le buffer. | |
* @return Le buffer modifié. | |
*/ | |
protected abstract StringBuilder toString(StringBuilder sb); | |
/** | |
* Permet de visiter l'arbre. | |
* @param visitor Le visiteur. | |
*/ | |
public abstract void accept(NodeVisitor visitor); | |
} | |
/** | |
* Représente une expression « (<attribute><operator><value>) ». | |
* La classe est marquée comme finale pour qu'on ne puisse l'instancier que | |
* via le parsing d'un filtre de recherche ({@link LdapSearchFilter#parse(String)}). | |
* L'attribut n'est jamais vide (null ou ""), la valeur peut être "" mais pas null. | |
* @author jay | |
*/ | |
public final static class Expression extends Node { | |
protected String variable; | |
protected TestingOperator op; | |
protected String value; | |
protected Expression() { | |
// pas accessible de l'extérieur | |
} | |
@Override | |
public void accept(NodeVisitor visitor) { | |
visitor.visit(this); | |
} | |
@Override | |
protected StringBuilder toString(StringBuilder sb) { | |
sb.append('(').append(variable).append(op.value()).append(value).append(')'); | |
return sb; | |
} | |
@Override | |
public String toString() { | |
return toString(new StringBuilder()).toString(); | |
} | |
} | |
/** | |
* Représente une expression « (<operator><filter1><filter2>) » | |
* La classe est marquée comme finale pour qu'on ne puisse l'instancier que | |
* via le parsing d'un filtre de recherche ({@link LdapSearchFilter#parse(String)}). | |
* @author jay | |
*/ | |
public final static class Filtre extends Node { | |
protected CombiningOperator op; | |
protected Node[] nodes; | |
protected Filtre() { | |
// pas accessible de l'extérieur | |
} | |
@Override | |
public void accept(NodeVisitor visitor) { | |
if(CombiningOperator.NOT.equals(op)) { | |
nodes[0].accept(visitor); | |
} else { | |
for(Node node : nodes) | |
node.accept(visitor); | |
} | |
visitor.visit(this); | |
} | |
@Override | |
protected StringBuilder toString(StringBuilder sb) { | |
sb.append('(').append(op.value()); | |
for(Node node : nodes) | |
node.toString(sb); | |
sb.append(')'); | |
return sb; | |
} | |
@Override | |
public String toString() { | |
return toString(new StringBuilder()).toString(); | |
} | |
} | |
/* | |
* Helpers | |
*/ | |
/** | |
* Effectue le parsing. | |
* @param filter Le filtre de recherche. | |
* @return Le noeud racine. | |
* @throws IllegalArgumentException Si le filtre est incorrect. | |
*/ | |
protected static Node doProcess(StringIterator iterator) throws IllegalArgumentException { | |
if(!iterator.hasNext() || iterator.next() != '(') | |
throw new IllegalArgumentException("Un filtre de recherche doit commencer par '('"); | |
Node result = null; | |
char c = iterator.next(); | |
CombiningOperator operator = CombiningOperator.fromValue(c); | |
if(operator == null) { | |
iterator.pushBack(c); | |
StringBuilder sb = new StringBuilder(); | |
Expression expr = new Expression(); | |
while(iterator.hasNext()) { | |
c = iterator.next(); | |
TestingOperator test = TestingOperator.fromValue(c); | |
if(test != null) { | |
expr.op = test; | |
if(sb.length() == 0) | |
throw new IllegalArgumentException("Il manque la variable pour le test"); | |
expr.variable = sb.toString(); | |
sb.setLength(0); | |
break; | |
} else { | |
sb.append(c); | |
} | |
} | |
while(iterator.hasNext()) { | |
c = iterator.next(); | |
if(c == ')') { | |
iterator.pushBack(c); | |
break; | |
} | |
sb.append(c); | |
} | |
expr.value = sb.toString(); | |
result = expr; | |
} else { | |
Filtre filter = new Filtre(); | |
filter.op = operator; | |
List<Node> nodes = new ArrayList<Node>(); | |
nodes.add(doProcess(iterator)); | |
if(!CombiningOperator.NOT.equals(operator)) { | |
while((c = iterator.next()) != ')') { | |
iterator.pushBack(c); | |
nodes.add(doProcess(iterator)); | |
}; | |
iterator.pushBack(c); | |
} | |
filter.nodes = nodes.toArray(new Node[nodes.size()]); | |
if(!CombiningOperator.NOT.equals(operator)) { | |
if(filter.nodes.length < 2) | |
throw new IllegalArgumentException("Pas assez d'opérandes pour l'opérateur '" + operator.value() + "'"); | |
} | |
result = filter; | |
} | |
if(!iterator.hasNext() || iterator.next() != ')') | |
throw new IllegalArgumentException("Un filtre de recherche doit se terminer par ')'"); | |
return result; | |
} | |
/* | |
* API | |
*/ | |
/** | |
* Vérifie la validité d'un filtre de recherche. Les valeurs | |
* <code>null</code>, "" et [\s]+ sont considérées comme valides. | |
* Appelle {@link #parse(String)} et catch l'exception. | |
* @param filter Le filtre de recherche. | |
* @return <code>true</code> si l'expression est valide. | |
*/ | |
public static boolean isValid(String filter) throws IllegalArgumentException { | |
boolean valid = true; | |
try { | |
parse(filter); | |
} catch(IllegalArgumentException e) { | |
valid = false; | |
} | |
return valid; | |
} | |
/** | |
* Parse un filtre de recherche et renvoie | |
* le noeud racine de l'expression. | |
* @param filter Le filtre de recherche. | |
* @return Le noeud racine ou <code>null</code> si le filtre est "vide". | |
* @throws IllegalArgumentException Si le filtre est incorrect. | |
*/ | |
public static Node parse(String filter) throws IllegalArgumentException { | |
if(filter == null || (filter = filter.trim()).length() == 0) | |
return null; | |
StringIterator iterator = new StringIterator(filter); | |
Node result = doProcess(iterator); | |
if(iterator.hasNext()) | |
throw new IllegalArgumentException("Caractères en trop après la dernière ')'"); | |
return result; | |
} | |
/** | |
* Parcourt l'arbre pour en extraire les identifiants de variable. | |
* @param root Le noeud racine. | |
* @return La liste des identifiants trouvés, jamais <code>null</code>. | |
*/ | |
public static Set<String> findIdentifiers(Node root) { | |
final Set<String> result = new HashSet<String>(); | |
root.accept(new NodeVisitor() { | |
@Override | |
public void visit(Node node) { | |
if(node instanceof Expression) { | |
Expression expr = (Expression)node; | |
result.add(expr.variable); | |
} | |
} | |
}); | |
return result; | |
} | |
public static void main(String[] args) throws Exception { | |
String filter1 = "(&(guid=1001)(uid=jay))"; | |
Node filtre = LdapSearchFilter.parse(filter1); | |
System.out.println(filtre.toString()); | |
Set<String> identifiers = LdapSearchFilter.findIdentifiers(filtre); | |
System.out.println(identifiers); | |
String filtre2 = "(&(variable=value))"; | |
System.out.println(LdapSearchFilter.isValid(filtre2)); | |
try { | |
LdapSearchFilter.parse(filtre2); | |
} catch(IllegalArgumentException e) { | |
System.err.println(e.getMessage()); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment