Posts Tagged ‘code’
Vous devez écrire du code qui fait appel à JMX, en bon citoyen et bon développeur vous voulez tester ce code.
Première approche; vous enregistrez vos MBean sur un MBeanServer, disons celui de la plateforme (avec Java 6 : ManagementFactory.getPlatformMBeanServer()).
mBeanServer.registerMBean(theMBean, theMBean.getObjectName());
Étant donné que MBeanServer étends MBeanServerConnection il est possible d’exécuter des querys, de faire des invocations sur les MBean etc. Si le code est suffisamment isolé des aspects techniques de connexion à JMX, vous passerez le MBeanServer en lieu et place de la MBeanServerConnection.
Supposons le code suivant.
public class OperateOnJMXConnection implements JMXOperation {
public void perform(MBeanServerConnection connection) {
// doing some stuff there
}
public Result getResult() { return result; }
}
Pour tester ce code il faudrait alors écrire :
@Test
public void do_not_fail() {
operateOnJMXConnection.perform(mbeanServer);
assertThat(result).satisfies(someCondition);
}
Mais voilà, vous restez en local, et par exemple si vous avez merdé sur la sérialisation de vos beans, vous ne verrez pas d’échec dans vos test et vous aurez une surprise en prod, ou avant si votre projet a un processus qualité décent.
Évidement il y a une solution, l’idée c’est de pouvoir se connecter au mBeanServer local à votre processus (typiquement dans maven 3, l’exécution de vos tests peuvent être forkée).
Alors j’ai essayé de récupérer les informations pour récupérer les informations de la VM qui tourne, mais bon on tombe dans des classes sun, j’ai préféré ne pas continuer sur ce chemin semé d’embûches, sans compter sur la faiblesse de cette approche.
Bref en relisant les articles de Khanh sur JMX, j’ai vu quelque chose d’intéressant JMXConnectorServerFactory. Cette classe permet donc de créer un JMXConnectorServer avec l’URL qu’on lui spécifie et d’un MBeanServer. A noter que cette URL doit respecter un certain formalisme tel que la javadoc l’indique : service:jmx:protocol:remainder.
Le protocole ne peut pas être n’importe quoi, il faut qu’il y ait le bon service enregistré pour qu’il soit géré. Dans notre cas RMI est standard, c’est donc le protocole que je prendrai. Pour le remainder, il s’agit plus d’une partie d’une URL, je vous laisse voir la Javadoc de JMXServiceUrl à ce sujet, mais dans les grandes lignes la forme doit être la suivante : //[host[:port]][url-path]
JMXConnectorServer connectorServer = JMXConnectorServerFactory.newJMXConnectorServer(
new JMXServiceURL("service:jmx:rmi://"),
null,
mBeanServer
);
connectorServer.start();
Hop dans le code précédent, on a créé puis démarrer notre JMXConnectorServer. Il n’y a plus qu’à se connecter dessus de manière standard :
Je vais utiliser connectorServer.getJMXServer() pour récupérer l’URL du service, il y a une raison à cela, c’est que comme l’indique la javadoc, l’URL passée pour la création du JMXConnectorServer peut être légèrement modifiée par celui-ci, il faut donc récupérer la nouvelle URL.
JMXConnector jmxConnetor = JMXConnectorFactory.connect(connectorServer.getJMXServiceUrl()); MBeanServerConnection connection = jmx.getgetMBeanServerConnection();
Et voilà vous avez accès à une MBeanServerConnection, qui vit dans la JVM locale, mais qui utilise RMI pour communiquer avec le MBeanServer, du coup vous êtes nettement plus proches des conditions du code de production et c’est ce qui nous intéresse dans cet article.
Pour référence les articles de Khanh, et en français s’il vous plait
:
- Partie 1 : http://jetoile.blogspot.com/2010/10/jmx-pour-les-nuls-les-concepts-partie-1.html
- Partie 2 : http://jetoile.blogspot.com/2010/11/jmx-pour-les-nuls-les-differents-mbeans.html
- Partie 3 : http://jetoile.blogspot.com/2010/11/jmx-pour-les-nuls-les-agents-jmx-partie.html
- Partie 4 : http://jetoile.blogspot.com/2010/11/jmx-pour-les-nuls-les-classes-de-base.html
- Partie 5 : http://jetoile.blogspot.com/2010/11/jmx-pour-les-nuls-le-mbean-server.html
- Partie 6 : http://jetoile.blogspot.com/2010/12/jmx-pour-les-nuls-chargement-dynamique.html
- Partie 7 : http://jetoile.blogspot.com/2010/12/jmx-pour-les-nuls-les-services-jmx.html
- Partie 8 : http://jetoile.blogspot.com/2010/12/jmx-pour-les-nuls-les-connecteurs.html
Quelques liens javadoc :
Enfin je me suis créé une petite classe de commodité qui permet de créé facilement un loopback pour les TU :
Dans cette troisième et dernière partie sur les expressions régulières en Java. Je vais aborder deux thèmes assez peu utilisés et pourtant très utiles.
- Le premier, dans la continuité des groupes ce sont les constructions de look behind et look ahead.
- Le deuxième point abordera le support de Unicode dans nos expressions régulières.
Avoir le coup d’œil
C’est bien de ça dont il s’agit; ce feature, introduit grâce aux groupes non-capturant, permet de vérifier si une autre expression matche avant ou après une expression capturante sans consommer de caractères. Il y a 4 constructions de types :
- Les constructions look ahead
-
- (?=X) X, via zero-width positive lookahead : L’expression cherche à matcher X après la position courante et sans consommer.
- (?!X) X, via zero-width negative lookahead : L’expression cherche à ne pas matcher X après la position courante et sans consommer.
- Les expressions look behind
-
- (?<=X) X, via zero-width positive lookbehind : L’expression cherche à matcher X avant la position courante et sans consommer, ou X est une expression régulière de longueur connue.
- (?<!X) X, via zero-width negative lookbehind : L’expression cherche à ne pas matcher X avant la position courante et sans consommer, ou X est une expression régulière de longueur connue.
Ces assertions ressemblent aux bornes b elles ont un fonctionnement similaire mais plus complexes. Passons aux tests pour voir leur fonctionnement.
Les groupes de look ahead
Par exemple avec le look ahead positif :
public class LookAheadLookBehind {
private String text = "static private String aStaticVarLabel;" +
"static private Long anotherStaticVarLabel;" +
"private String anInstanceVar;" +
"protected String anInteger;";
@Test
public void classicRegex() {
assertEquals("aStaticVarLabel", regexMatch(text, "\w+Label"));
}
@Test
public void positiveLookAhead() {
assertEquals("aStaticVar", regexMatch(text, "\w+(?=Label)"));
}
private String regexMatch(String text, String regex) {
Matcher matcher = Pattern.compile(regex).matcher(text);
return matcher.find() ? matcher.group(0) : "";
}
}
Ligne 10, on veut chopper les lignes qui se terminent par “Label” avec une expression usuelle. Si on ne voulais pas la partie “Label”, alors il aurait fallu créer un autre groupe autour de w+, cependant le curseur aura consommé les caractères. L’alternative est d’utiliser un look ahead positif, c’est ce qu’on a à la ligne 15, ici le curseur s’arrête après le “r” juste avant “Label”.
Notez que dans l’exemple ce qui est retourné est le groupe 0 (ligne 21), c’est à dire l’ensemble de ce qui est capturé par toute la regex. Ceci illustre à nouveau que les groupes de look ahead/begind ne capturent pas (méthode positiveLookAhead, ligne 15). C’est assez pratique pour faire des sélections ou des remplacements, dans Eclipse par exemple.
Si typiquement on cherche des termes qui ne se terminent pas par “Label”. On écrira simplement :
@Test
public void negativeLookAhead() {
assertEquals("private", regexMatch(text, "\w+(?!Label)"));
}
L’expression chope en premier “private”, tout simplement parce que cette partie du texte matche le fait qu’il n’y a pas Label qui suit, si on veut chopper le nom d’une variable alors on peut ajouter des constructions de look behind. C’est ce qu’on regarde juste après.
Les groupes de look behind
@Test
public void positiveLookBehind() {
assertEquals("anotherStaticVarLabel", regexMatch(text, "(?<=private Long )\w+"));
}
Donc là j’ai préfixé la regex par ce que je voulais voir juste avant. De la même manière si on ne veut pas d’un terme, on utilisera un look behind négatif (?<!), par exmple si on ne veut pas de “String”.
@Test
public void negativeLookBehind() {
assertEquals("anotherStaticVarLabel", regexMatch(text, "(?<=private \w{4,8} )(?<!String )\w+"));
}
Observez ici qu’il y a deux constructions adjacentes look behind, l’une positive l’autre négative, ce qui illustre encore mieux que ces constructions ne consomment pas la séquence de caractères.
Observez également que l’expression ici est de longueur connue : le w{4,8} ne prend que de 4 à 8 caractères. Il n’est pas possible d’écrire un look behind avec un quantificateur où la longueur n’est pas connue, la construction suivante est fausse et provoquera une erreur de syntaxe : (?<!private w+ ). C’est une limite technique qui impose aux groupes de look behind d’avoir une longueur fixe ou calculable; le quantificateurs borné {n,m}, l’option ? ou l’alternative | tombent dans cette catégorie. Ainsi on pourrait écrire :
@Test
public void negativeLookBehind() {
assertEquals("anotherStaticVarLabel", regexMatch(text, "(?<=(?:static )?private long|Long )\w+"));
}
Et donc par opposition les quantificateurs * et + ne sont pas autorisés.
Attention aux quantificateurs sur une même classe de caractère
Bon, il existe certains cas un peu délicats ou les caractères adjacents d’une séquence font partie de la même classe. Dans le bout de texte utilisé dasn le premier exemple, les nom variables correspondent typiquement à ça:
anotherStaticVarLabel
Le nom de la variable appartient à la classe de caractère [a-zA-Z0-9_] ou encore à w.
Lorsqu’on faisait un positive look ahead, le quantificateur w+ va chercher à matcher l’ensemble des caractères de cette classe, ce qui veut dire que w+ va matcher et consommer les caractères “anotherStaticVarLabel”. Du coup lorsque la construction (?=Label) cherche à matcher “Label”, elle n’y arrive pas. Ce n’est pas grave, avec le backtracking l’expression w+ reviens en arrière jusqu’à ce que (?=Label) matche.
L’histoire est différente avec un negative look ahead; une fois que la partie w+ a matché “anotherStaticVarLabel”, le curseur est positionné après le “l”. Maintenant le moteur teste (?!Label), qui cherche donc à ne pas matcher “Label”, normal c’est une négation. Et là ça marche, cette partie de l’expression ne peut plus trouver “Label”, donc la construction est validée.
Bref ce n’est pas ce qu’on veut, nous voulons par exemple identifier les variables qui ne sont pas suffixées par “Label” !
Pour ne éviter ce problème, il faut placer le groupe look ahead négatif avant w+. Cela ne posera pas de problème étant donné que les look ahead ne consomment pas la séquence de caractères. Ainsi en écrivant :
@Test
public void controlYourQuantifiers() throws Exception {
assertEquals("anInstanceVar", regexMatch(text, "(?<=String )(?!\w+Label)\w+"));
}
La première partie est un look behind pour avoir ce qui est après “String “, le deuxième groupe est le look ahead dont je parlais, ce groupe cherche à ne matcher w+Label, si les derniers caractères Label de la regex ne sont pas trouvés alors c’est bon. Finalement l’expression se termine par w+. L’astuce donc se fait en deux étapes:
- Déplacer le look ahead avant l’expression qui consomme les caractères et qu’on veut capturer, ici w+
- Faire précéder dans le look ahead négatif l’expression qu’on veut capturer, ici le groupe est devenu (?!w+Label), grâce au backtracking dans ce groupe une valeur “aStaticVarLabel” ne sera pas matchée (negative look ahead).
Voilà pour les possibilités de look ahead et de look behind dans les expressions rationnelles.
Passons maintenant au support Unicode par la classe Pattern.
Unicode
En quoi Unicode est intéressant dans nos regex en Java?
- Unicode est supporté nativement par Java, le format interne des String est Unicode.
- Unicode nous apporte des classes, des catégories ou des propriétés de caractères bien plus étendues que les classes ASCII couramment utilisées.
Juste pour une lettre
Par exemple, j’ai une application US qui vérifie que le texte entré est uniquement composé de lettres. Facile avec la regex suivante:
[a-zA-Z]
Maintenant je me dit que je souhaiterais avoir des clients français! Aille! L’approche facile mais peu élégante est d’écrire une regex dans ce genre :
[a-zA-Zéèêïôàù]
Et encore j’oublie les accents sur les majuscules et encore d’autre caractères spéciaux, alors qu’ils ont pourtant pleine valeur orthographique sur les majuscules également. S’il fallait en plus gérer le grec, l’allemand, l’espagnol, nous aurions du mal avec une telle expression régulière. Et le raccourci w n’aide pas vraiment non plus! C’est là que viennent les classes de caractère Unicode, pour identifier un caractère qui est une lettre, on écrira très simplement :
p{L}
Ainsi en Java on aura par exemple
@Test
public void matchLettersInDifferentLanguage() throws Exception {
Assert.assertTrue(Pattern.matches("(\p{L}| )+", "une manœuvre sur un chêne"));
Assert.assertFalse(Pattern.matches("[\p{Lower} ]+", "une manœuvre sur un chêne")); // Attention p{Lower} est une classe POSIX ou ASCII !
Assert.assertTrue(Pattern.matches("[\p{Ll} ]+", "une manœuvre sur un chêne")); // Classe des petites lettres en Unicode p{Ll} !
Assert.assertTrue(Pattern.matches("[\p{L} ]+", "eine kleine Straße in München"));
Assert.assertTrue(Pattern.matches("[\p{L} ]+", "Это настоящая красота"));
}
IntelliJ est très bien, il fourni l’auto-complétion dans les regex c’est assez pratique à l’intérieur du code, mais pas d’explication sur la signification de ces blocs de caractères Unicode. Eclipse n’en parlons pas, et NetBeans je ne sais pas. En tous cas on trouve une réponse ici ou encore là à propos des blocs Unicode:
| Abréviation reconnue par Pattern | Signification |
|---|---|
| L | Letter |
| Lu | Uppercase Letter |
| Ll | Lowercase Letter |
| Lt | Titlecase Letter |
| Lm | Modifier Letter |
| Lo | Other Letter |
| M | Mark |
| Mn | Non-Spacing Mark |
| Mc | Spacing Combining Mark |
| Me | Enclosing Mark |
| N | Number |
| Nd | Decimal Digit Number |
| Nl | Letter Number |
| No | Other Number |
| S | Symbol |
| Sm | Math Symbol |
| Sc | Currency Symbol |
| Sk | Modifier Symbol |
| So | Other Symbol |
| P | Punctuation |
| Pc | Connector Punctuation |
| Pd | Dash Punctuation |
| Ps | Open Punctuation |
| Pe | Close Punctuation |
| Pi | Initial Punctuation |
| Pf | Final Punctuation |
| Po | Other Punctuation |
| Z | Separator |
| Zs | Space Separator |
| Zl | Line Separator |
| Zp | Paragraph Separator |
| C | Other |
| Cc | Control |
| Cf | Format |
| Cs | Surrogate |
| Co | Private Use |
| Cn | Not Assigned |
| - | Any* |
| - | Assigned* |
| - | ASCII* |
Matcher les caractère d’un alphabet seulement
Si je veux vérifier que mon texte appartient à de l’hébreu ou du chinois c’est faisable. Dans Unicode il faut remarquer qu’il y a plusieurs notion pour les “alphabets”; il y a les Blocs et les Scripts, cependant le moteur de Java qui se base essentiellement sur le moteur de perl, ne gère pas les scripts, donc on se contentera des blocs.
Ci-dessous je teste l’appartenance à un bloc :
@Test
public void matchABlock() throws Exception {
Assert.assertFalse(Pattern.matches("(\p{InBASIC_LATIN}| )+", "une manœuvre sur un chêne"));
Assert.assertTrue(Pattern.matches("(\p{InLATIN_EXTENDED_A}|\p{InLATIN_1_SUPPLEMENT}|\p{InBASIC_LATIN}| )+", "une manœuvre sur un chêne"));
Assert.assertTrue(Pattern.matches("[\p{InLATIN_1_SUPPLEMENT}\p{InBASIC_LATIN} ]+", "eine kleine Straße in München"));
Assert.assertTrue(Pattern.matches("[\p{InCYRILLIC} ]+", "Это настоящая красота"));
Assert.assertFalse(Pattern.matches("[\p{InHEBREW} ]+", "Это настоящая красота"));
Assert.assertTrue(Pattern.matches("[\p{InCJK_UNIFIED_IDEOGRAPHS} ]+", new String(Character.toChars(0x6C23)))); // chi écriture traditionnel
Assert.assertTrue(Pattern.matches("[\p{InHIRAGANA} ]+", new String(Character.toChars(0x304D)))); // ki écriture Hiragana
}
Plusieures choses sont à remarquer :
- Le nom de l’alphabet est précédé par “In“
- Pour avoir une phrase en français on a très vite plusieurs blocs “LATIN EXTENDED A” pour le graphème ‘œ’, “LATIN 1 SUPPLEMENT” pour le ‘ê’ E accent circonflexe.
- D’autres alphabet sont plus pratique à utiliser comme l’hébreu, le cyrillique, le grecque, etc.
- L’utilisation des alphabet Chinois, Japonais, Coréen peut aussi soulever des question surtout quand on ne le parle pas
A noter également :
Sur les deux dernière lignes noter que j’ai utiliser le code hexadécimal UTF-16 (j’y reviendrais après) pour obtenir les caractères 氣 et き (Chi en chinois traditionnel, Ki avec l’alphabet Hiragana). Pourquoi? Parce que Unicode c’est bien joli mais dans le monde réel il y a des limitations, pour moi il s’agit de la police de caractère de mon éditeur qui ne possède pas ces blocs de caractères défini. Peut-être aurez vous des limitations sur la police de votre navigateur. A noter également que l’encodage de vos fichier peut faire mal quand on joue avec les caractères en dehors du latin basique.
Chi (0x6C23) Ki (0x304D)
On peut encore s’amuser
Pour revenir dans les choses qui nous intéresse, imaginons que nous voulions compter tous les caractères accentués dans un texte. Le bloc Unicode p{L} n’est pas approprié, mais comme je l’ai dit avec Unicode on peut accéder aux propriété d’un caractère.
Déjà pour commencer il faut savoir qu’en Unicode, un graphème comme ‘é’ peut correspondre à un seul caractère ‘é’ ou à deux caractères ‘e’ suivi du modificateur accent grave. Cela dépend de la source, mais ces cas sont probables.
@Test
public void graphemes() {
Assert.assertTrue(Pattern.matches("\p{InLATIN_1_SUPPLEMENT}+", "éèêu00E9"));
Assert.assertFalse(Pattern.matches("\p{InLATIN_1_SUPPLEMENT}+", "eu0301"));
Assert.assertTrue(Pattern.matches("(\p{L}\p{M})+", "eu0301"));
Assert.assertTrue(Pattern.matches("(\p{InLATIN_1_SUPPLEMENT}|\p{L}\p{Mn})+", "éêeu0301u0065u0302"));
System.out.println("éêeu0301u0065u0302");
}
Ainsi dans les lignes précédentes pour rechercher un graphème représenté par un seul codepoint, il faudra aller le chercher dans le bloc idoine, ici “LATIN 1 COMPLEMENT”, 0x00E9 est le codepoint du caractère ‘é’. La forme décomposée de ‘é’ est ‘e’ (0×0065) suivi du modificateur accent grave (0×0301).
Pour matcher cette forme décomposée du graphème, il faut simplement écrire p{L}p{M}. Il est toujours possible d’affiner l’expression en choisissant des propriétés plus précises (cf. Tableau plus haut, voire la référence Unicode). Du coup pour matcher n’importe quelle forme d’un graphème on pourra écrire l’expression de la ligne 6.
Enfin rapidement on peut exprimer les compléments à la manière standard avec [^p{Lu}] ou plus simple avec un grand P P{Lu}. Les intersections entres les classes / propriétés Unicode se font sans problèmes également :
@Test
public void complements() throws Exception {
Assert.assertTrue(Pattern.matches("[^\p{Lu}]+", "une manœuvre sur un chêne")); // exclusion
Assert.assertFalse(Pattern.matches("[^\p{Lu}]+", "Une Manœuvre sur un chêne"));
Assert.assertFalse(Pattern.matches("\P{Lu}+", "Une Manœuvre sur un chêne")); // complément
Assert.assertFalse(Pattern.matches("[[^\p{Lu}]&&\p{IsL} ]+", "une manœuvre sur un chêne 123164")); // exclusion et intersection
Assert.assertTrue(Pattern.matches("[[^\p{Lu}]&&\p{IsL} ]+", "une manœuvre sur un chêne")); // exclusion et intersection
}
Petit retour sur les base de Java
Java gère nativement Unicode, les String sont encodées en UTF-16. Ce qui explique par conséquent que lorsque je veux exprimer un caractère sous forme hexadécimale, il faut l’écrire dans sa forme UTF-16.
@Test
public void utf16() throws Exception {
Assert.assertTrue(Pattern.matches("u00E9", "é"));
Assert.assertTrue(Pattern.matches("\u00E9", "é"));
Assert.assertTrue(Pattern.matches("u00E9", new String(Character.toChars(0x00E9))));
Assert.assertTrue(Pattern.matches("\u00E9", new String(Character.toChars(0x00E9))));
}
Ces assertions marches toutes mais il faut noter que u00E9 est compris par le compilateur et remplacera u00E9 par ‘é’, alors que dans la forme ou le backslash est échappé \u00E9 le compilateur ne fera rien. Ce sera au moteur Pattern de traiter la chaîne.
@Test
public void charLengthForCodePoint() throws Exception {
Assert.assertEquals(1, Character.toChars(0x00E9).length); // é
Assert.assertEquals(1, Character.toChars(0x304D).length); // ki
Assert.assertEquals(2, Character.toChars(0x0001D50A).length); // MATHEMATICAL FRAKTUR CAPITAL G
}
La plupart des caractères tiendront dans le type primitif char qui fait donc 16 bits (voilà pourquoi Java gère nativement l’UTF-16) , cependant il peut arriver que certains caractères demandent davantage. Character.toChars(int) prend donc un codepoint représenté par en entier, qui fait en Java 32 bits pour exprimer Unicode en UTF-32 donc. Dans le code ci-dessus la 3ème assertion montre d’ailleurs que Java doit splitter le caractère en question sur deux char.
De la même manière l’encodage change naturellement la taille d’un tableau de byte (8 bits).
@Test
public void encodingDifferenceForAsciiChars() throws Exception {
String string = "une chaine ascii";
assertEquals(string.length(), string.getBytes("ASCII").length);
assertEquals(string.length(), string.getBytes("UTF-8").length);
}
@Test
public void encodingDifferenceForAccentedChars() throws Exception {
String string = "un chêne, un frêne, une orchidée";
assertTrue(string.length() == string.getBytes("ASCII").length);
assertEquals(string.length() + 3, string.getBytes("UTF-8").length);
}
Bilan
Voilà cet article clos la série que je voulais écrire sur les expressions régulière. Il y a probablement d’autres arcanes à connaître. Mais sur cette série le but était de couvrir ce que le moteur Java nous permet de faire. Je pense que comprendre le fonctionnement du moteur en particulier sur le backtracking, la manière du moteur de tester une expression, la manière dont le moteur parcoure / consomme les caractères en entrée, sont des facteurs clé pour réussir une bonne expression. Cette compréhension est d’autant plus importante quand celles-ci sont liée à des éléments de performance.
Les constructions apportées avec Unicode, même limitées, ouvrent certaines possibilités intéressantes, mais clairement il y a du travail à faire : Unicode n’est manifestement pas simple.
Références
- Le tutorial perl : http://perldoc.perl.org/perlretut.html
- La classe Pattern : http://download.oracle.com/javase/1.5.0/docs/api/java/util/regex/Pattern.html
- Unicode Regular Expressions: http://unicode.org/reports/tr18/
- Unicode Character Datablase : http://www.unicode.org/Public/5.1.0/ucd/UCD.html
- FileFormat, pour en savoir plus sur un certain caractère : http://www.fileformat.info/info/unicode/char/search.htm
- Types primitifs en Java : http://download.oracle.com/javase/tutorial/java/nutsandbolts/datatypes.html
Il était une fois les expressions régulières
Depuis bien longtemps je connais et pratique les expressions régulières, à la fois au moment de coder, mais également dans mes éditeurs de texte, parfois aussi dans le shell, lors d’un grep par exemple. Bref les expressions régulières sont pratiques dans la vie de tous les jours pour un ingénieur logiciel.
Seulement voilà je me suis aussi rendu compte que certains d’entre nous n’ont pas une connaissance approfondie des expressions régulières et de leurs arcanes. Effectivement il y a parfois certaines expressions qui sont assez absconses. Et aujourd’hui les moteurs des expressions régulières dépassent ce le cadre dans lequel ces expressions ont été conçue. Elle permettent certaines constructions qui sont peu connues.
Les expressions régulières hier et aujourd’hui
Sans remonter aux origines des expressions régulières -cette partie là est couverte par wikipedia- il est intéressant de noter que les expressions régulières et leur moteur ont bien évoluées en 60ans. En effet le mot régulier vient de l’état de fait que ces expressions permettaient de rechercher dans des langages formels et non-contextuel; aujourd’hui les recherches ont avancées et les moteurs permettent maintenant de dépasser le cadre du langage formel pour permettre de travailler dans l’espace du langage contextuel. Les racines des expressions régulières remontent bien avant l’avènement de l’informatique pour aller jusqu’aux raisonnements complexes de la logique mathématique.
Il y a une une grosse différence entre un langage non-contextuel et un lange contextuel, dans les faits cette évolution explique pourquoi il y a aujourd’hui des différences dans les moteurs qui sont intégrés dans les différents programmes (en fonction de la plateforme, des outils, du langages etc.) Aujourd’hui en Perl, en C#, et en Java nous avons la chance d’avoir des moteurs qui font partie des dernières générations. C’est sur cet héritage que je vais disserter, cela dit uniquement dans le cadre de Java et de sa fameuse classe Pattern. (Vous remarquerez d’ailleurs que le moteur est nommé Pattern plutôt que Regex ou quelque chose du genre, l’explication est simple : cette génération de moteur n’est plus simplement à propos d’expression régulière mais donc de pattern.) Je tiens aussi à préciser que cet article se concentre sur la création d’expressions régulières et non sur l’usage de la classe Pattern.
Les différentes constructions
Petit rappel
Je passe rapidement sur les bases, j’imagine que tout le monde connaît les constructions basiques d’une expression régulière :
- Les classes de caractère [ ] et les compléments [^ ]
- L’opérateur de Kleene *
- L’alternative | (le pipe)
- Les autres quantificateurs : +, ?, {}, ces quantificateurs ne sont vraiment que des raccourcis de ce qui est déjà exprimable avec les autres constructions, mais ils nous simplifient la vie.
- Les groupes ()
Globalement pas de surprises ici, avec ses constructions il assez facile d’écrire l’expression la plus simple jusqu’à l’expression un poil plus élaborée.
Par exemple pour valider un mail (sans rentrer dans les arcanes de la RFC) on peut avoir ça:
@Test
public void simpleEmailMatch() {
String regex = "[a-z]+(\.[a-z]+)*@[a-z]+\.[a-z]{2,6}";
assertTrue(Pattern.compile(regex).matcher("brice.dutheil@gmail.com").matches());
}
Ok, c’est déjà pas mal, mais si on veut extraire une section d’un texte ou valider précisément certaines sections d’un texte, il faut connaitre les constructions un peu plus pointues.
Les ancres
Les ancres sont rangées dans la javadoc de la classe Pattern sous la catégorie Boundary matchers. Une ancre identifie juste une position à laquelle elle matche, elle ne consomme pas de caractères dans la séquence traitée.
Le début et la fin d’une ligne
Généralement les personnes qui ont beaucoup travaillé avec le shell connaissent les deux principales ancres, à savoir le début d’une ligne ^ et la fin d’une ligne $. Mais il y a une astuce en Java, c’est que par défaut ^ et $ repèrent le début et la fin du CharSequence uniquement, pas de notion de saut de ligne!
Pour s’en convaincre on écrit un petit test simple qu’on enrichira d’assertions, la méthode regexFirstMatch extrait la première section du texte qui matche la regex :
@Test
public void startEndOfLineVsPermanentStartEndOfString() {
String text = "The account number is :n" +
"t123456789n" +
"the client phone number is :n" +
"t0-987-654-321n";
assertEquals("T", regexFirstMatch(text, "^.")); // Tout le temps le début de la séquence
assertEquals("1", regexFirstMatch(text, ".$")); // Tout le temps la fin de la séquence
}
private String regexFirstMatch(String text, String regex) {
Matcher matcher = Pattern.compile(regex).matcher(text);
return matcher.find() ? matcher.group(0) : "";
}
Et ouai, on ne s’attend pas à ça (matche T et 1) surtout quand la description de ces ancres utilise le mot ligne. En fait il faut activer l’option multiligne Pattern.MULTILINE dans le moteur, pour que celui-ci identifie les sauts de ligne.
Ainsi dans le contexte du bout de code du dessus, les lignes suivantes permettent de voire qu’il s’agit bien du caractère ‘:’ de la première ligne qui est trouvé.
Matcher matcher = Pattern.compile(".$", Pattern.MULTILINE).matcher(text);
matcher.find();
assertEquals(":", matcher.group(0));
Nice, mais il y a encore mieux, le moteur de regex de Java (comme certains autres) permet de donner les options à l’intérieur de la regex, la javadoc de Pattern donne cette info dans la catégorie Special constructs (non-capturing), celle qui nous intéresse est la construction sur les options pour toute l’expression.
- (?idmsux-idmsux) Nothing, but turns match flags on – off
Il faut le placer au début de l’expression régulière, ici (?m) :
assertEquals(":", regexFirstMatch(text, "(?m).$"));
On choppe alors bien le caractère à la fin de la première ligne.
Le début et la fin d’une séquence de caractères
Dans notre expression si on veut se caler dans tous les cas sur le début et la fin d’une séquence de caractères, il y a des ancres dédiées A et Z. Celles-ci ne sont bien entendu pas affectées par l’option multiligne.
assertEquals("T", regexFirstMatch(text, "\A.")); // Tout le temps le début de la séquence
assertEquals("1", regexFirstMatch(text, ".\Z")); // Tout le temps la fin de la séquence
assertEquals("1", regexFirstMatch(text, "(?m).\Z")); // Tout le temps la fin de la séquence
Notez quand même qu’en ce qui concerne le Z le dernier caractère de la séquence qui est un séparateur de ligne n’est pas retourné! Comme indiqué dans la javadoc, cette ancre repère la position avant le dernier caractère séparateur (écrit comme terminators dans la javadoc).
Il existe d’autres ancres, mais elles sont moins utiles, je vous laisse voir par vous même.
Les options
On a vu qu’on pouvait activer des options pour une expression régulière, effectivement c’est assez pratique.
Les options possibles utilisables à la construction ou dans le pattern sont dans la javadoc, mais les plus intéressantes sont :
| Option | Flag | Flag à la construction |
| Multi-ligne | m | Pattern.MULTILINE |
| Insensibilité à la casse | i | Pattern.CASE_INSENSITIVE |
| Matching de la casse relatif aux règles Unicode | u | Pattern.UNICODE_CASE |
| Matching des caractère en fonction de leur forme canonique | Pattern.CANON_EQ |
Certaines options comme vu dans le tableau n’ont pas d’équivalence dans la regex.
Bon c’est bien pratique ça, mais parfois on aimerait bien s’assurer que la casse est ou n’est pas vérifiée sur une portion de la regex. Il existe une construction qui permet d’activer/désactiver une option dans une section de l’expression régulière :
- (?idmsux-idmsux:X) X, as a non-capturing group with the given flags on – off
A peu près la même chose que pour les options avec une portée sur toute la regex, sauf que cette fois, la portion soumise à l’option changée est à l’intérieur d’un groupe. Et là vous remarquerez que la javadoc dit bien “non-capturing” ça veut dire que la regex ne gardera pas en mémoire le contenu de ce groupe, contrairement aux groupes qui, donc, capturent et sont identifiables par l’encadrement du groupe par des parenthèses (X).
Ainsi par exemple si on ne veut pas tenir compte de la casse dans une portion de la regex on écrirait:
assertFalse(Pattern.compile("(?-i)[a-z]+ [a-z]+ [a-z]+").matcher("jqsdfkjkd fdfhJGJKGFQSDKjb ckbvg").matches());
assertTrue(Pattern.compile("(?-i)[a-z]+ (?i:[a-z]+) [a-z]+").matcher("jqsdfkjkd fdfhJGJKGFQSDKjb ckbvg").matches());
Dans la première expression, qui ne marche pas, l’ensemble de l’expression est sensible à la casse c’est l’option (?-i) en début d’expression. Mais au milieu on voudrait quand même autoriser les majuscules. Pour ce faire on active l’insensibilité uniquement pour le groupe du milieu (?i:[a-z]+).
Les bornes de mot
Les bornes de mots sont des ancres de type particulier. Comme n’importe quelle ancre, ces bornes ne consomment aucun caractère. La borne b s’utilise avant ou après un mot pour marquer le début ou la fin d’un mot.
Par exemple en utilisant la classe de caractère w.
assertTrue("word".matches("\bword"));
assertTrue("word".matches("word\b"));
assertTrue("word".matches("\bword\b"));
assertTrue("word".matches("\b\w+\b"));
assertTrue("12dsk_".matches("\b\w+\b"));
assertTrue("12dsk; fdg987".matches("\w+\b.*\b\w+"));
assertFalse("12dsk; ;:!,:".matches("\w+\b.*\b\w+"));
assertTrue(Pattern.compile("\bes\b").matcher("Tu es encore dans ces histoires ").find());
assertFalse(Pattern.compile("\bes\b").matcher("Tu as encore des histoires ").find());
assertFalse("12dsk-".matches("\w+"));
assertFalse("12dsk-".matches("\w+-\b"));
assertTrue("12dsk-".matches("[0-9a-z-]+"));
assertFalse("12dsk-".matches("[0-9a-z-]+\b"));
assertFalse("12dsk. ".matches("\w+.\b."));
assertFalse("bobEtLéa".matches("bob\b\w+\bLéa"));
Bon c’est bien cool, mais si je veux matcher un texte en allemand, du grec ou simplement des lettres accentuées de notre bon français ? Là ça pèche un peu si on utilise le w.
assertFalse("Éole".matches("\b\w+"));
assertTrue("Éole".matches("\bÉole"));
assertTrue("Éole".matches("\b[Éa-z]+"));
assertTrue("Éole".matches("\b\p{L}+"));
En effet la classe w ne connait que les caractères ASCII et plus précisément; uniquement ceux de cette classe [a-zA-Z0-9_] tel que c’est mentionné dans la javadoc. Pour palier à cette limitation soit il faut ajouter le caractère accentué à une classe de caractère, soit on utilise une classe de caractère Unicode, c’est ce qui est fait dans la dernière assertion j’utilise p{L} ! Je reviendrais plus tard sur Unicode avec les expressions régulières.
Attention à l’encodage de vos codes source ! J’ai eu des erreurs d’encodage du fichier sur Eclipse, IntelliJ et NetBeans qui provenaient de plateformes différentes (MacOSX et Windows), du coup le caractère É n’était pas bien encodé (comprendre que l’IDE encodait ce caractère dans autre chose qu’une lettre), ce qui faisait évidement échouer l’expression.
Enfin le complément d’une borne b est représenté par la borne B, celle-ci matche tout ce que b ne matche pas. Dans les faits B marque la borne entre deux classes de caractères à l’exception d’une classe composée des caractères qu’on peut trouvé dans w.
assertTrue("12dsk-".matches("\w+-\B")); // B capture l'inverse b
assertTrue("12dsk.".matches("\w+\.\B"));
assertTrue(".!? nt".matches("[.!?]+\B\s+"));
assertTrue(".!?,,,;:".matches("[.!?]+\B[,;:]+"));
Fin de la partie 1
Voilà pour la première partie, la plus simple, sur les expressions régulières en Java. Pour la suite qui arrive très bientôt j’exposerai la manière de fonctionner de certaines constructions un peu particulières : les backreferences, les quantificateurs possessifs, les possibilités de lookahead / lookbehind.
Références
Lors de ma mission précédente, une des unités business a décidé d’investir dans Scrum, avec ce changement de méthodologie, les équipes de développement étant relativement enthousiastes, ont été formées au TDD et au pair-programming. Une discussion avec un pote, nous a amené à parler du pair-architecturing, ou la confrontation des idées, le challenge apporté par son alter-ego, m’a conduit à écrire ce petit retour à propos du pair-programming, et pourquoi l’appliquer à d’autres discipline n’est pas une mauvaise idée, au contraire.
Après plus d’un an passé dans ce contexte d’agilité avec Scrum, je vous livre quelques pensées sur le pair-programming.
D’abord l’évolution
Toutes les équipes n’ont pas choisi d’adopter le pair-programming comme pratique régulière pour l’ensemble des développements.
- Avant Scrum, les développeurs se voyaient confiés certains dossiers, et chacun travaillait vaguement dans son coin avec parfois l’aide des autres.
- Coup de bol expérience de refactoring de l’architecture. J’ai eu l’occasion en temps que développeur de travailler avec un autre développeurs sur l’architecture de l’application. Les enjeux sont importants, ce serait dommage de se louper et de planter une version.
A cette époque là j’ai vraiment eu plaisir à travailler avec l’autre développeur, à faire ce que j’appellerai du pair-architecturing.
- En effet, au démarrage de Scrum les développeurs n’étaient pas encore près à faire le changement, moi y compris:
J’étais à l’aise seul devant mon poste, et parfois je demandais l’aide ou le retour des membres de mon équipes, et cela me convenait bien.
- Ensuite l’assignation des taches est petit à petit sorti mais en partie seulement de la responsabilité des développeurs, par conséquent le choix de faire du pair-programming était de moins en moins une option possible pour le développement. C’est dommage.
- Pendant les phases (par développeur) destinées à la maintenance, il fallait transmettre la connaissance métier, j’ai vu et même participé à l’analyse de problèmes métier complexes, très spécifiques, mais sur des domaines très variés. A ce moment il ne s’agit pas encore véritablement de pair-programming, mais de pair-analysing. Cette étape finalement couteuse en temps pour ceux qui possèdent la connaissance métier et la connaissance du code afférent est nécessaire pour les développeurs qui passent donc par cette case maintenance.
- Un sujet technique assez poussé et sur un sujet délicat à maitriser, pour terminer ce projet, il a été décidé de staffer les deux personnes qui avait bossé dessus (seules mais à la suite l’une de l’autre pour des raisons de planning). A deux donc nous avons décidé de faire du pair-programming. C’est la première véritable expérience de pair que j’ai eu.
Et j’en ai été très heureux, car à deux avec un niveau relativement équivalent, on a pu travailler avantageusement le design du code, et la testabilité de nos design. Egalement aussi on a pu penser au cas limites bien mieux que lorsqu’on avait travaillé chacun de notre coté, et pourtant nous avions fait régulièrement des réunions (informelles) avec l’architecte pour discuter des points sombres.
- Puis arrivé dans une équipe avec des newbies sur le projet; il fallait transmettre les connaissances. Et là c’est la volonté forte du nouveau scrum master de pousser le pair-programming, j’ai définitivement apprécié cette pratique quand j’ai travaillé dans cette équipe, j’ai vécu la plupart du temps des succes story, mais aussi quelques petits écueils et inconvénients, j’en fait donc part dans la suite de l’article.
Nous avons pratiqué le pair-programming pour l’écriture de nouveau code, pour la réhabilitation de code legacy, et pour le refactoring de code legacy (ce n’est pas la même chose).Ce que je retiendrais c’est que l’équipe était reconnue comme véloce, et de mon point de vue je pense que le travail réalisé était plutôt bon. Et que définitivement le travail aurait pris bien plus de temps et aurait été moins bien fait si chacun était resté sur ses tâches.
J’ai trouvé ces moments de pair-analysing comme particulièrement intéressant, car très riche en métier, et surtout très formateurs sur les chose à ne pas faire, typiquement lorsqu’il faut corriger du code legacy, le design de celui-ci devient une caractéristique majeure du temps de compréhension et du temps de correction d’anomalies. Egalement je ne faisais déjà plus vraiment confiance aux commentaires, mais ces passages ont à nouveau enfoncé le clou.
Les retours enfin
Honnêtement je pense que travailler en binôme est vraiment très bien et très bon pour un projet, en particulier si le code du projet doit survivre longtemps. Mais il y a certaines choses à éviter.
- Le pair-programming c’est lent et ça coute cher, c’est ce qu’on peut vous dire, mais ceux qui avancent ça n’ont aucun argument pour étayer ces propos.
- A propos de la lenteur : il n’y a pas de mesures que je connait qui vont dans ce sens ou dans l’autre d’ailleurs.
C’est la raison pour laquelle je souhaite témoigner, et je tiens à dire que c’est l’équipe qui favorisait au maximum le pair-programming qui était ressentie comme la plus véloce.
- A propos du coût, effectivement il y a deux éléments qui travaillent ensemble sur un sujet, mais le sujet fonctionnel est finalement mieux maitrisé par les développeurs, les aller-retours entre les deux personnes favorisent l’échange d’information et les réflexions relatives, bref ça favorise le bon sens et l’intelligence. Ces deux choses donnent naturellement un code mieux réfléchi, plus robuste et plus évolutif, pour sûr pour les besoins du présent, et très probablement pour les besoins futurs comme les évolutions ou la maintenance (on ne sait jamais). Évidement faire du vrai code objet et utiliser avantageusement TDD/BDD permet de booster cet aspect. Il en résulte que sur le moyen et le long terme, les couts deviennent avantageux, et encore plus si d’autres aspects entre en jeux comme le SLA, les aspects contractuels, légaux, et autres.
- A propos de la lenteur : il n’y a pas de mesures que je connait qui vont dans ce sens ou dans l’autre d’ailleurs.
- Travailler à deux n’est pas de tout repos, si vraiment il y a un échange intense, 6h de travail à deux c’est déjà une grosse journée, franchement on est ruiné, l’un comme l’autre. Faites le comprendre à votre entourage que travailler en binôme est fatiguant (mais productif).
- Quoi qu’il arrive il y a toujours des mails à dépiler, il y a toujours des interruptions, et même parfois il faut aller interrompre d’autres personnes. Organisez votre temps à deux pour faire ces activités sans gêner le binôme.
- Quand vous travaillez à deux n’oublier pas d’échanger régulièrement les rôles de pilote (celui qui code) de celui qui observe. C’est très important d’échanger les rôles, ça permet de faire fonctionner l’esprit avec un point de vue différent, ça stimule le cerveau. Le binôme choisira sa cadence.
- Ce n’est pas une relation professeur/enseignant, c’est une relation qui se base sur les responsabilités :
- celui qui code qui a le clavier entre les mains qui a la seule responsabilité de penser le code et de l’écrire
- celui qui à accès aux docs papier qui a le temps de prendre du recul
Encore une fois il doit y avoir un échange régulier entre le driver et l’observer.
- Il faut savoir essayer les idées des autres! Surtout s’il n’y a pas de contre-indications et que les deux idées se valent (avant de les mettre en application).
- La différence de niveau des deux commendataires ne doit pas être trop grande, effet, c’est irritant, ennuyant, pénible, une perte de temps pour celui qui a les compétences, et ça ne permet pas de former adéquatement le plus faible. En bref ce binôme est à la fois une perte d’argent et de temps. Pour former le newbie il faut utiliser autre chose.
- Tous le monde ne sait pas travailler en binôme, il ne faut pas forcer une personne qui ne sait pas ou ne veut pas faire du binômage. Cette approche demande des compétences sociales importantes (quelque soit le rôle) comme de la patience, de l’humilité, du calme. Et ce que je peux dire c’est que j’ai vraiment eu la chance de m’améliorer sur ces qualité grâce au pair-programming.
- Le design du code et des tests est plus propre, plus intelligibles, les impacts des changements mieux maitrisés, bref que du bien pour le code (même s’il peut toujours y avoir mieux, mais le mieux est l’ennemi du bien à ce qu’il parrait).
- C’est un tandem dans lequel chacun protège l’autre de faire de la sur-ingénierie, ou inversement de prendre des raccourcis. Il ne faut pas seulement avoir du code correct, il faut aussi avoir un code solide pour les gens de la maintenance, pour les gens de l’exploitation, pour les futurs ingénieurs.
Finalement
Le pair programming est une pratique remarquablement efficace, quand elle est bien appliquée. C’est un véritable bénéfice pour tout de suite, mais surtout pour demain. Et c’est bien le demain qui est souvent le grand oublié des clients, ils ne voient pas les couts et les problèmes de notre métier qu’il faudra adresser sur du code legacy lors d’un refactoring, lors d’une évolution ,ou lors d’une correction d’anomalie.
Egalement je trouve que le code objet est véritablement le meilleur outils lorsqu’on travaille en pair-programming. Ce langage permet vraiment de mieux exprimer ce qui ressort des échanges observateurs/pilote. A deux il faut se protéger et s’inciter mutuellement pour faire du bon code.
L’utilisation en plus de TDD, et même de BDD qui est plus orienté responsabilité et comportement booste encore la qualité du code et de réfléchir en terme de business, de métier.
Pour aller plus loin je pense que le travaille en binôme favorise l’intellect et la consommation la plus exhaustive des scénarii métier. L’appliquer au développement est manifestement très bien, mais l’appliquer à d’autre métier est encore mieux. Typiquement pour des rôles aussi importants et impactants que l’architecture, avoir en face de soi un challenger, qui remet véritablement en question les élément sur les quels il n’est pas d’accord est un vrai plus. En plus de transmettre la connaissance, cette confrontation solidifie l’ensemble.
Je suis content et même reconnaissant d’avoir pu travailler avec la plupart des personnes en binôme, il m’ont énormément appris. Pour peu que chacun accepte le dialogue et argumente constructivement, il n’en ressortira que du bon.
Vous avez des remarques ou des retours à faire partagez, n’hésiter pas!
Prologue A l’école nos professeurs nous apprenaient ce qu’était la programmation orientée objet; en particulier l’encapsulation. En effet avoir un accès public aux variables internes d’un objet n’est pas particulièrement recommandé, pourtant nous avons connaissons tous la convention JavaBean : Manque de bol, cette convention qui a pourtant son utilité -voire sa nécéssité- peut dans certains contextes briser l’encapsulation, et…





Follow