/**
 * Copyright (c) 2010-2021 Contributors to the openHAB project
 *
 * See the NOTICE file(s) distributed with this work for additional
 * information.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0
 *
 * SPDX-License-Identifier: EPL-2.0
 */
package org.openhab.binding.tr064.internal.phonebook;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.stream.Collectors;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
import javax.xml.transform.stream.StreamSource;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.http.HttpMethod;
import org.openhab.binding.tr064.internal.dto.additions.PhonebooksType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * The {@link Tr064PhonebookImpl} class implements a phonebook
 *
 * @author Jan N. Klug - Initial contribution
 */
@NonNullByDefault
public class Tr064PhonebookImpl implements Phonebook {
    private final Logger logger = LoggerFactory.getLogger(Tr064PhonebookImpl.class);

    private Map<String, String> phonebook = new HashMap<>();

    private final HttpClient httpClient;
    private final String phonebookUrl;

    private String phonebookName = "";

    public Tr064PhonebookImpl(HttpClient httpClient, String phonebookUrl) {
        this.httpClient = httpClient;
        this.phonebookUrl = phonebookUrl;
        getPhonebook();
    }

    private void getPhonebook() {
        try {
            ContentResponse contentResponse = httpClient.newRequest(phonebookUrl).method(HttpMethod.GET)
                    .timeout(2, TimeUnit.SECONDS).send();
            InputStream xml = new ByteArrayInputStream(contentResponse.getContent());

            JAXBContext context = JAXBContext.newInstance(PhonebooksType.class);
            XMLInputFactory xif = XMLInputFactory.newFactory();
            xif.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false);
            xif.setProperty(XMLInputFactory.SUPPORT_DTD, false);
            XMLStreamReader xsr = xif.createXMLStreamReader(new StreamSource(xml));
            Unmarshaller um = context.createUnmarshaller();
            PhonebooksType phonebooksType = um.unmarshal(xsr, PhonebooksType.class).getValue();

            phonebookName = phonebooksType.getPhonebook().getName();

            phonebook = phonebooksType.getPhonebook().getContact().stream().map(contact -> {
                String contactName = contact.getPerson().getRealName();
                return contact.getTelephony().getNumber().stream()
                        .collect(Collectors.toMap(number -> normalizeNumber(number.getValue()), number -> contactName,
                                this::mergeSameContactNames));
            }).collect(HashMap::new, HashMap::putAll, HashMap::putAll);
            logger.debug("Downloaded phonebook {}: {}", phonebookName, phonebook);
        } catch (JAXBException | InterruptedException | ExecutionException | TimeoutException | XMLStreamException e) {
            logger.warn("Failed to get phonebook with URL {}:", phonebookUrl, e);
        }
    }

    // in case there are multiple phone entries with same number -> name mapping, i.e. in phonebooks exported from
    // mobiles containing multiple accounts like: local, cloudprovider1, messenger1, messenger2,...
    private String mergeSameContactNames(String nameA, String nameB) {
        if (nameA != null && nameA.equals(nameB)) {
            return nameA;
        }
        throw new IllegalStateException(
                "Found different names for the same number: '" + nameA + "' and '" + nameB + "'");
    }

    @Override
    public String getName() {
        return phonebookName;
    }

    @Override
    public Optional<String> lookupNumber(String number, int matchCount) {
        String normalized = normalizeNumber(number);
        String matchString = matchCount > 0 && matchCount < normalized.length()
                ? normalized.substring(normalized.length() - matchCount)
                : normalized;
        logger.trace("Normalized '{}' to '{}', matchString is '{}'", number, normalized, matchString);
        return matchString.isBlank() ? Optional.empty()
                : phonebook.keySet().stream().filter(n -> n.endsWith(matchString)).findFirst().map(phonebook::get);
    }

    @Override
    public String toString() {
        return "Phonebook{" + "phonebookName='" + phonebookName + "', phonebook=" + phonebook + '}';
    }

    private String normalizeNumber(String number) {
        // Naive normalization: remove all non-digit characters
        return number.replaceAll("[^0-9]\\+\\*", "");
    }
}

@@ -433,73 +433,8 @@
 k;%0A%0A
-import java.io.ByteArrayInputStream;%0Aimport java.io.InputStream;%0A
 impo
@@ -525,440 +525,25 @@
 til.
-concurrent.ExecutionException;%0Aimport java.util.concurrent.TimeUnit;%0Aimport java.util.concurrent.TimeoutException;%0Aimport java.util.stream.Collectors;%0A%0Aimport javax.xml.bind.JAXBContext;%0Aimport javax.xml.bind.JAXBException;%0Aimport javax.xml.bind.Unmarshaller;%0Aimport javax.xml.stream.XMLInputFactory;%0Aimport javax.xml.stream.XMLStreamException;%0Aimport javax.xml.stream.XMLStreamReader;%0Aimport javax.xml.transform.stream.StreamSource
+stream.Collectors
 ;%0A%0Ai
@@ -641,103 +641,8 @@
 nt;%0A
-import org.eclipse.jetty.client.api.ContentResponse;%0Aimport org.eclipse.jetty.http.HttpMethod;%0A
 impo
@@ -709,16 +709,69 @@
 ksType;%0A
+import org.openhab.binding.tr064.internal.util.Util;%0A
 import o
@@ -1515,799 +1515,150 @@
     
-try %7B%0A            ContentResponse contentResponse = httpClient.newRequest(phonebookUrl).method(HttpMethod.GET)%0A                    .timeout(2, TimeUnit.SECONDS).send();%0A            InputStream xml = new ByteArrayInputStream(contentResponse.getContent());%0A%0A            JAXBContext context = JAXBContext.newInstance(PhonebooksType.class);%0A            XMLInputFactory xif = XMLInputFactory.newFactory();%0A            xif.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false);%0A            xif.setProperty(XMLInputFactory.SUPPORT_DTD, false);%0A            XMLStreamReader xsr = xif.createXMLStreamReader(new StreamSource(xml));%0A            Unmarshaller um = context.createUnmarshaller();%0A            PhonebooksType phonebooksType = um.unmarshal(xsr, PhonebooksType.class).getValue();%0A
+PhonebooksType phonebooksType = Util.getAndUnmarshalXML(httpClient, phonebookUrl, PhonebooksType.class);%0A        if (phonebooksType != null) %7B
 %0A   
@@ -1719,25 +1719,24 @@
 .getName();%0A
-%0A
             
@@ -2296,210 +2296,8 @@
 k);%0A
-        %7D catch (JAXBException %7C InterruptedException %7C ExecutionException %7C TimeoutException %7C XMLStreamException e) %7B%0A            logger.warn(%22Failed to get phonebook with URL %7B%7D:%22, phonebookUrl, e);%0A
     

Use Util.getAndUnmarshalXML method to retrieve phonebook (supports timeout 5s and caching) (#10447)