2 * Copyright (c) 2010-2021 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.tr064.internal.phonebook;
15 import java.io.ByteArrayInputStream;
16 import java.io.InputStream;
17 import java.util.HashMap;
19 import java.util.Optional;
20 import java.util.concurrent.ExecutionException;
21 import java.util.concurrent.TimeUnit;
22 import java.util.concurrent.TimeoutException;
23 import java.util.stream.Collectors;
25 import javax.xml.bind.JAXBContext;
26 import javax.xml.bind.JAXBException;
27 import javax.xml.bind.Unmarshaller;
28 import javax.xml.stream.XMLInputFactory;
29 import javax.xml.stream.XMLStreamException;
30 import javax.xml.stream.XMLStreamReader;
31 import javax.xml.transform.stream.StreamSource;
33 import org.eclipse.jdt.annotation.NonNullByDefault;
34 import org.eclipse.jetty.client.HttpClient;
35 import org.eclipse.jetty.client.api.ContentResponse;
36 import org.eclipse.jetty.http.HttpMethod;
37 import org.openhab.binding.tr064.internal.dto.additions.PhonebooksType;
38 import org.slf4j.Logger;
39 import org.slf4j.LoggerFactory;
42 * The {@link Tr064PhonebookImpl} class implements a phonebook
44 * @author Jan N. Klug - Initial contribution
47 public class Tr064PhonebookImpl implements Phonebook {
48 private final Logger logger = LoggerFactory.getLogger(Tr064PhonebookImpl.class);
50 private Map<String, String> phonebook = new HashMap<>();
52 private final HttpClient httpClient;
53 private final String phonebookUrl;
55 private String phonebookName = "";
57 public Tr064PhonebookImpl(HttpClient httpClient, String phonebookUrl) {
58 this.httpClient = httpClient;
59 this.phonebookUrl = phonebookUrl;
63 private void getPhonebook() {
65 ContentResponse contentResponse = httpClient.newRequest(phonebookUrl).method(HttpMethod.GET)
66 .timeout(2, TimeUnit.SECONDS).send();
67 InputStream xml = new ByteArrayInputStream(contentResponse.getContent());
69 JAXBContext context = JAXBContext.newInstance(PhonebooksType.class);
70 XMLInputFactory xif = XMLInputFactory.newFactory();
71 xif.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false);
72 xif.setProperty(XMLInputFactory.SUPPORT_DTD, false);
73 XMLStreamReader xsr = xif.createXMLStreamReader(new StreamSource(xml));
74 Unmarshaller um = context.createUnmarshaller();
75 PhonebooksType phonebooksType = um.unmarshal(xsr, PhonebooksType.class).getValue();
77 phonebookName = phonebooksType.getPhonebook().getName();
79 phonebook = phonebooksType.getPhonebook().getContact().stream().map(contact -> {
80 String contactName = contact.getPerson().getRealName();
81 return contact.getTelephony().getNumber().stream()
82 .collect(Collectors.toMap(number -> normalizeNumber(number.getValue()), number -> contactName,
83 this::mergeSameContactNames));
84 }).collect(HashMap::new, HashMap::putAll, HashMap::putAll);
85 logger.debug("Downloaded phonebook {}: {}", phonebookName, phonebook);
86 } catch (JAXBException | InterruptedException | ExecutionException | TimeoutException | XMLStreamException e) {
87 logger.warn("Failed to get phonebook with URL {}:", phonebookUrl, e);
91 // in case there are multiple phone entries with same number -> name mapping, i.e. in phonebooks exported from
92 // mobiles containing multiple accounts like: local, cloudprovider1, messenger1, messenger2,...
93 private String mergeSameContactNames(String nameA, String nameB) {
94 if (nameA != null && nameA.equals(nameB)) {
97 throw new IllegalStateException(
98 "Found different names for the same number: '" + nameA + "' and '" + nameB + "'");
102 public String getName() {
103 return phonebookName;
107 public Optional<String> lookupNumber(String number, int matchCount) {
108 String normalized = normalizeNumber(number);
109 String matchString = matchCount > 0 && matchCount < normalized.length()
110 ? normalized.substring(normalized.length() - matchCount)
112 logger.trace("Normalized '{}' to '{}', matchString is '{}'", number, normalized, matchString);
113 return matchString.isBlank() ? Optional.empty()
114 : phonebook.keySet().stream().filter(n -> n.endsWith(matchString)).findFirst().map(phonebook::get);
118 public String toString() {
119 return "Phonebook{" + "phonebookName='" + phonebookName + "', phonebook=" + phonebook + '}';
122 private String normalizeNumber(String number) {
123 // Naive normalization: remove all non-digit characters
124 return number.replaceAll("[^0-9]\\+\\*", "");