Deutsche Ortsnetzkennzahlen validieren

“The telephone book is full of facts, but it doesn’t contain a single idea.”

Mortimer J. Adler

In diesem Beitrag geht es um eine weitere Ergänzung im kleinen Zoo der Validatoren der Telephone Bibliothek. Bislang existierten Validatoren für die Länderkennzahl und die deutschen Mobilvorwahlen, es fehlte die Validierung der deutschen Ortsnetzkennzahlen.

Die Validierung der deutschen Ortsnetzkennzahlen fehlte bislang in der Bibliothek, weil es insgesamt 5200 von ihnen gibt. Dis bisher existierende Validierung für Mobilvorwahlen kümmerte sich nur um 54 Vorwahlen, die mit Hilfe einer einfachen Platzhaltersyntax auf 10 Vergleichswerte reduziert wurden.

Für die Prüfung der Ortsnetzkennzahlen existiert natürlich eine triviale Lösung. Einfach alle Kennzahlen aus einer Datei einlesen und den Vergleich durchführen. Triviale Lösungen machen aber keinen Spaß, daher wird hier nun eine etwas andere Lösung vorgestellt.

Bisher nutze die Annotation @GermanMobileDestinationCode für die Validierung die allgemeinere Annotation @NationalDestinationCode und deren Validator. Mit der Unterstützung der deutschen Ortsnetzkennzahlen ergibt sich aber nun die Möglichkeit, deutsche Mobilvorwahlen, deutschen Ortsnetzkennzahlen oder beide gemeinsam zu validieren. Eine entsprechende Auswahl passt aber nicht in die @NationalDestinationCode Annotation, daher wird ein neue Annotation @GermanDestinationCode bereitgestellt.

@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = {})
public @interface GermanDestinationCode {
    String message() default "{de.schegge.validator.GermanDestinationCode.message}";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

    String[] value() default {};

    GermanNationalDestinationCodes ranges() default GermanNationalDestinationCodes.ALL;

    boolean allowed() default true;
}

Über das Attribut ranges() kann zwischen den drei Varianten für die deutschen Vorwahlen gewählt werden. Die folgende Annotation @GermanAreaCode nutzt dies, indem sie mit die @GermanDestinationCode Annotation mit dem Wert GermanNationalDestinationCodes.AREA nutzt.

@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = {})
@GermanDestinationCode(ranges=GermanNationalDestinationCodes.AREA)
@GermanCountryCode
public @interface GermanAreaCode {
    @OverridesAttribute(constraint = GermanDestinationCode.class)
    @OverridesAttribute(constraint = GermanCountryCode.class)
    String message() default "{de.schegge.validator.GermanAreaCode.message}";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
}

Außerdem verwendet sie die Annotation @GermanCountryCode, denn eine Prüfung auf deutsche Ortsnetzkennzahlen macht nur Sinn, wenn es auch eine deutsche Nummer ist.

Deutsche Vorwahlen werden alle von der Klasse GermanDestinationCodeValidator geprüft, die Validierung findet aber in der Basisklasse AbstractDestinationCodeValidator statt.

public class GermanDestinationCodeValidator extends AbstractDestinationCodeValidator<GermanDestinationCode> {

    @Override
    public void initialize(GermanDestinationCode constraintAnnotation) {
        if (constraintAnnotation.value().length > 0) {
            nationalDestinationCodes = Arrays.asList(constraintAnnotation.value());
        } else {
            nationalDestinationCodes = constraintAnnotation.ranges().getAreaCodes();
        }
        allowed = constraintAnnotation.allowed();
    }
}

Die Initialisierung des GermanDestinationCodeValidator unterscheidet zwischen der direkten Angabe von Vorwahlen und die Nutzung der GermanNationalDestinationCodes Konstanten. Ist das Attribut value() gesetzt, dann wird ranges() ignoriert.

Die eigentliche Validierung geschieht in der Basisklasse AbstractDestinationCodeValidator. Der Algorithmus ist für alle Implementierungen identisch. Die Vorwahl wird in einer Liste gesucht, in der alle erlaubten, bzw. verbotenen Vorwahlen stehen.

public abstract class AbstractDestinationCodeValidator<A extends Annotation> implements ConstraintValidator<A, InternationalPhoneNumber> {
    protected List<String> nationalDestinationCodes;
    protected boolean allowed;

    @Override
    public boolean isValid(InternationalPhoneNumber phoneNumber, ConstraintValidatorContext constraintValidatorContext) {
        if (phoneNumber == null) {
            return true;
        }
        return allowed == nationalDestinationCodes.contains(phoneNumber.getNationalDestinationCode());
    }
}

Die deutschen Vorwahlen in der Liste werden aber nicht aus einer Datei eingelesen, sondern werden aus einer kompakten String-Darstellung generiert.

Diese kompakte String-Darstellung ist die RANGES_OFFSET Variante des Ranges Collector aus der Stream Collector Utilities Bibliothek.

Die RANGES_OFFSET Darstellung beginnt mit dem Startwert und dann folgen kommasepariert Offsetwerte für die nächsten Zahlen. Nach dem Offsetwert kann eine weitere Zahl folgen, die die Anzahl aufeinanderfolgender Werte angibt.

Die folgende Zeile stellt die deutschen Mobilvorwahlen dar.

160,+2+1,+8+8,+1331+19,+41+9,+11+9,+13967,+322

Die erste Vorwahl ist also die 160, dann folgen die 162 und 163. Der nächste Offset spring zur 171 und so weiter. Die entpackte Darstellung für die Mobilvorwahlen ist die folgende Liste.

160,162,163,
171,172,173,174,175,176,177,178,179,
1510,1511,1512,1513,1514,1515,1516,1517,1518,1519,1520,1521,1522,1523,1524,1525,1526,1527,1528,1529,
1570,1571,1572,1573,1574,1575,1576,1577,1578,1579,
1590,1591,1592,1593,1594,1595,1596,1597,1598,1599,
15566,15888

Die nächsten 5319 Zeichen repräsentieren die 5200 deutschen Ortsnetzkennzahlen, die von der Bundesnetzagentur zur Verfügung gestellt werden.

30,+10,+29,+20,+112+2,+5+1,+2+1,+2,+7,+7,+3,+3,+7,+10,+10,+10,+10,+10,+40,+4,+5+1,+4,+6,+4,+6,+4,+6,+4,+6,+4,+6,+4,+26,+10,+10,+10,+10,+10,+10,+10,+20,+10,+10,+10,+10,+10,+10,+10,+10,+20,+10,+10,+10,+10,+10,+10,+10,+30,+10,+10,+10,+10,+10,+10,+10,+10,+20,+10,+10,+10,+10,+10,+10,+10,+25,+5,+10,+10,+10,+10,+10,+10,+10,+10,+1050,+2,+2,+6+3,+2,+2,+6+2,+36+2,+25,+2+2,+4,+13+4,+2+3,+2+5,+5,+2+2,+6+2,+8+2,+2+1,+6+6,+14+6,+4+6,+3+7,+3+6,+4+8,+2+4,+16+6,+4+8,+14+2,+2,+3+9,+12+4,+2+12,+2+4,+2+2,+2+4,+2+2,+2+4,+6+8,+12+8,+2+5,+4+1,+2+6,+2+5,+5+4,+6+3,+8,+2+2,+15+1,+2+5,+11+9,+3+4,+2,+3+2,+2+3,+3+7,+3+7,+3+4,+6+7,+2+9,+2+7,+12+8,+2+9,+2+6,+4+6,+4+3,+2+1,+4+7,+2+9,+2+6,+24+4,+7+7,+2+4,+2,+3+5,+3+1,+2+3,+6+9,+22+3,+17+7,+3+8,+2+4,+5+3,+2+4,+2+6,+4+3,+28+3,+16+4,+2+1,+3+4,+2+1,+3+4,+2+1,+3+4,+2+1,+3+3,+7+4,+2,+4+4,+6+3,+307+3,+2+1,+14+1,+5+2,+2+1,+2+1,+2+1,+3+1,+2,+2,+15+1,+2,+2,+5+1,+3,+2+2,+2+1,+3+1,+5,+3+1,+26,+2,+2,+6,+2,+2,+2,+4,+2,+2,+2+1,+13+1,+2,+2,+5,+2,+2+1,+15,+2+1,+2,+5,+3,+17+2,+2,+3+1,+2,+2,+2,+2,+4+1,+2,+2,+15+3,+7,+2+1,+2,+2,+3,+2,+2+1,+2,+3+1,+2,+2,+5,+2,+2+1,+15+3,+4+1,+2+1,+2+2,+5,+2+1,+3,+14,+2,+8+1,+3,+2,+2,+2+2,+2+1,+5,+2,+2,+26+6,+4,+2,+2,+2,+4,+3+1,+16+4,+6+3,+47,+10,+3,+2,+2,+3,+2+1,+3,+13+1,+2,+2+4,+2,+3,+2+1,+4,+2,+3,+15+1,+2,+3,+2,+12,+2,+2,+3,+3,+2,+2,+2,+4,+2+1,+2+1,+2,+12+8,+2,+2,+3,+5,+3,+3,+4,+3,+2,+2,+103+8,+11+9,+2+13,+2,+2+1,+2+5,+2+1,+2+8,+2+18,+2+4,+7+7,+12+3,+6+19,+2+7,+2+9,+2+6,+4+8,+3+6,+4+1,+2,+2+1,+12+4,+2+14,+2+2,+2+3,+2+7,+3+6,+4+1,+9+4,+7+2,+7+8,+12+2,+2+1,+5+4,+6+6,+4+5,+2,+3+8,+2+4,+2+12,+2+8,+2+5,+2+1,+12+8,+2+6,+2,+2+6,+3+9,+2+3,+38+7,+12+6,+3+9,+2+3,+2,+5,+10+7,+3+3,+7+3,+18+6,+13+4,+6+6,+3+9,+2+7,+3+18,+12+5,+6+4,+15+9,+2+7,+2+8,+2+8,+2+4,+6+6,+4+4,+7+1,+9+1,+17+9,+2+5,+2+1,+2+7,+2+9,+2+7,+3+6,+44+7,+3+6,+4+4,+6+5,+4,+2+7,+2+3,+8+4,+15+2,+2,+3+1,+12,+2,+3+6,+3+4,+2+8,+2+8,+2+7,+3+6,+4+6,+3+9,+2+8,+12+5,+2,+3+7,+3+1,+2+4,+2+5,+2+2,+2+5,+5+7,+3+5,+6+3,+5+9,+11+9,+2+6,+2,+2,+3+3,+4+7,+3+7,+3+8,+2+3,+17+6,+2,+12+8,+2+8,+2+7,+3+8,+2+1,+2+4,+3+5,+5+4,+6+4,+7+7,+11+5,+2+2,+2+5,+5+5,+5+5,+5+4,+6+3,+8+4,+6+2,+7+8,+12+5,+5+5,+5+7,+2+9,+2+4,+6+6,+4+5,+5+5,+6+5,+14+5,+5+3,+7+5,+5+4,+6,+2+6,+2+6,+25+6,+12+9,+2+15,+2+7,+2+2,+2+4,+7+3,+7+1,+18+8,+12+5,+5+6,+2,+2+7,+3+6,+4+5,+5,+2,+2+3,+24+2,+3+1,+12+4,+2+3,+2+5,+3,+2+18,+2+2,+3,+2,+3,+2+1,+4,+3+6,+5+4,+5+8,+11,+2+2,+2+10,+2+1,+3,+2+3,+3+2,+2+1,+2+2,+2+6,+4+4,+6+7,+2,+2,+3+1,+2,+3+6,+2,+11+4,+2+3,+2+8,+2+6,+2,+2+7,+3+8,+2+1,+2+2,+5+6,+4+7,+3+7,+13+8,+2+18,+2+2,+2+4,+2+3,+7+4,+6+6,+4+7,+2+9,+11+16,+2+9,+2,+2+7,+3+1,+2+4,+3+8,+3+4,+14+9,+13+5,+4+5,+5+4,+5+9,+2+8,+2+4,+3,+2+9,+2+6,+2,+21+11,+2+6,+2+7,+2+11,+2+7,+2+6,+3+3,+7+7,+3,+2+1,+2+3,+12+7,+3+6,+4+6,+4+7,+3+5,+5+5,+5+8,+13+4,+3,+12,+3+3,+4+7,+3+3,+4+1,+2+7,+3,+3+5,+2+5,+5,+6+1,+5+1,+3+1,+123+5,+5+3,+7+5,+5+5,+6+1,+3,+5+2,+8+4,+36+15,+2+1,+2+7,+2+4,+2+3,+2+5,+5+5,+5+3,+7+4,+7+2,+16+9,+2+6,+3,+2+27,+2+6,+23,+2+7,+12+8,+2+6,+3,+3+5,+3+7,+3+6,+4,+2+3,+5+8,+2+4,+7+2,+16,+2+7,+2+5,+4+9,+2+8,+2+6,+4+7,+4+4,+16+4,+14,+2,+2+1,+2+2,+2+3,+7+5,+5+7,+3+18,+2+6,+15,+18+9,+2+5,+5+5,+5+6,+3+9,+2+5,+5+4,+17+7,+11+9,+2+5,+2+1,+2+7,+3,+2+2,+6+4,+6,+2+2,+2,+25+6,+13+5,+5+8,+2+3,+7+3,+49+4,+23+25,+2+2,+2+6,+4+6,+43+9,+2+5,+2+1,+2+2,+2+1,+5+6,+4+6,+4+5,+5+5,+5+4,+7,+2+2,+15+3,+7,+2+6,+2+5,+5+2,+4+1,+3,+4+3,+2+1,+5+3,+12+5,+6+6,+13+5,+4+4,+2+3,+2,+2,+2+9,+3+2,+2+2,+2+4,+2+3,+2,+5+4,+6+5,+6+2,+2,+14+8,+2+8,+2+9,+12+9,+2+17,+3+3,+7+5,+14+3,+2+1,+4+4,+6+5,+4,+2+2,+2+13,+32+6,+2,+22+7,+3+17,+3+4,+6+3,+7+5,+5+2,+28+3,+4+3,+2+9,+7+3,+2,+2+1,+4+6,+2+2,+6+2,+2+6,+15+7,+12+7,+3+4,+6+4,+6+3,+2,+5+1,+2+2,+5+3,+7+4,+16+2,+2+4,+12+4,+16,+4+2,+4,+5+2,+2+2,+5+2,+201+8,+2+14,+3,+2,+2+6,+13,+2+1,+3+3,+2+4,+6+8,+2+7,+3+6,+3+29,+2+8,+11+3,+2,+2+2,+2+5,+2,+3+5,+5+6,+3+29,+3+3,+7+1,+2+2,+14,+2+3,+5+29,+3+4,+2,+2+7,+3+5,+5+7,+3+8,+11+4,+2+3,+2,+2+3,+2+1,+2+7,+3+3,+7+8,+2+3,+6+2,+2,+7+2,+2,+2+2,+3+3,+16+8,+2+5,+6+7,+2+5,+4+9,+2+5,+26+6,+13+1,+2+4,+3+8,+2+7,+3+8,+2+5,+5+6,+4+2,+18,+3,+4,+12+9,+3+6,+3+1,+2+5,+12+5,+5+8,+23+3,+15,+2+7,+2+6,+4+7,+3+6,+4,+4,+2+2,+2+5,+25,+2+5,+12+9,+2+2,+2+3,+3+7,+3+5,+5+5,+5+7,+23073,+2+3,+24,+2+7,+4+1,+106+9,+21+5,+2+2,+92+7,+23+8,+24+5,+34+7,+12+1,+2,+2+2,+12,+2+7,+122+8,+22+7,+14+5,+14+8,+22+3,+4,+23+3,+7+7,+12,+2+7,+61+9,+2,+2+6,+21,+2+6,+42+6,+2+1,+2+2,+29+17,+2+3,+2,+3,+213+6,+13+3,+17+3,+17+2,+28+8,+22+1,+2+1,+2+1,+13+7,+13+3,+17+5,+36+4,+15,+2+3,+15+6,+24+7,+102+7,+2,+23+1,+2+4,+12+3,+2,+2+1,+12+2,+18+1,+29+1,+19+2,+2+1,+25+5,+3,+2+2,+2,+116,+2+4,+2,+11+9,+24+3,+17,+2+4,+41+8,+4+1,+19+6,+142+9,+31+9,+14+5,+54+5,+2,+12+2,+18+4,+18+6,+44+3,+3,+12+5,+15+7,+122+9,+82+7,+24+6,+23+5,+15+4,+17+1,+2+2,+23,+2+1,+2+4,+12+3,+28+5,+14+4,+35+9,+12+4,+16,+2+2,+45+9,+12+2,+28+1,+2+3,+4+4,+2,+113+9,+43+7,+71+8,+32+9,+42+7,+22+4,+4+1,+2+4,+16+3,+117+7,+13+5,+2,+12,+2+7,+2+2,+38+4,+6+4,+25+9,+2+3,+17+1,+2,+2,+15+4,+55+9,+21+1,+2+2,+3,+42+9,+11+1,+2+6,+12+8,+231,+2+2,+2+3,+82+7,+22+9,+12+3,+2+3,+11+9,+12+3,+37+2,+7+9,+23+3,+2+1,+132+9,+143,+2+3,+444+8,+11+9,+2+3,+58+5,+3+9,+11+8,+3+3,+17+5,+14+9,+12+2,+29+7,+21+9,+2+1,+2,+2,+15+5,+2,+232+9,+2+2,+2+3,+12+9,+21+5,+2+2,+2+3,+2+1,+24+7,+13+4,+2+1,+2+6,+2+1,+12+5,+124+9,+21+9,+11+9,+2+1,+18+9,+111+9,+12+5,+15+7,+14+6,+23+7,+22+5,+2+2,+12+8,+12+5,+16+2,+2+23,+12+7,+23+8,+22+4,+2+2,+111+8,+113+3,+2+2,+12+9,+2+3,+17+8,+41+9,+2+2,+18+8,+2+2,+18+8,+32+8,+2+3,+17+6,+2,+12+2,+2+3,+13+8

Die Darstellung kann noch erheblich gekürzt werden, da das Pluszeichen nach dem Komma eigentlich unnötig ist. Hinter dem Komma steht immer ein Offset.

Die drei Listen für die deutschen Vorwahlen werden in der Enum GermanNationalDestinationCodes verwaltet. Die Konstanten MOBILE und AREA enthalten die entsprechenden Listen, die Konstante ALL greift über eine simple List Implementierung auf die anderen beiden Listen zu.

public enum GermanNationalDestinationCodes {
    MOBILE(unpackRanges(PACKED_MOBILE_CODES)),
    AREA(unpackRanges(PACKED_AREA_CODES)),
    ALL(new AbstractList<>() {
        @Override
        public String get(int index) {
            return null;
        }

        @Override
        public int size() {
            return MOBILE.codes.size() + AREA.codes.size();
        }

        @Override
        public boolean contains(Object o) {
            return MOBILE.codes.contains(o) || AREA.codes.contains(o);
        }
    });

    private List<String> codes;

    GermanNationalDestinationCodes(List<String> codes) {
        this.codes = codes;
    }

    List<String> getAreaCodes() {
    return codes;
    }

    private static List<String> unpackRanges(String ranges) {
        String[] parts = ranges.split(",", 2);
        return Arrays.stream(parts[1].split(",")).collect(RangesUnpackCollector.unpack(Integer.parseInt(parts[0])));
    }
}

Das Entpacken ist in einen speziellen Collector ausgelagert, der aus den Offsets und dem initialen Wert eine Liste von Zahlen generiert.

@Test
void testValidGermanNumber(Validator validator) {
  InternationalPhoneNumberDto dto = new InternationalPhoneNumberDto();
  dto.mobile = InternationalPhoneNumber.of("49", "0", "172", "9980752");
  dto.area = InternationalPhoneNumber.of("49", "0", "30", "23125666"); 
  assertTrue(validator.validate(dto).isEmpty());
}

Die beiden Telefonnummern +49 172 9980752 und +49 30 23125666 können unbesorgt in jedem Test verwendet werden. Vielleicht haben sie eine der beiden Nummern auch schon einmal beiläufig in einer Fernsehproduktion gehört. Es handelt sich bei diesen Telefonnummern um sogenannte Drama Numbers, die extra für die Verwendung in Medienproduktionen freigehalten wurden. Zwei andere Telefonnummern +49 4435 2300 und +49 89 32168 haben es aber leider bis heute nicht zur Drama Number geschafft. Vermutlich bekommen die Inhaber noch immer störende Anrufe. Über einen Validator zum Verbieten von Drama Numbers wird vielleicht im nächsten Beitrag berichtet.

Schreibe einen Kommentar