001package ball.upnp.ssdp;
002/*-
003 * ##########################################################################
004 * UPnP/SSDP Implementation Classes
005 * %%
006 * Copyright (C) 2013 - 2022 Allen D. Ball
007 * %%
008 * Licensed under the Apache License, Version 2.0 (the "License");
009 * you may not use this file except in compliance with the License.
010 * You may obtain a copy of the License at
011 *
012 *      http://www.apache.org/licenses/LICENSE-2.0
013 *
014 * Unless required by applicable law or agreed to in writing, software
015 * distributed under the License is distributed on an "AS IS" BASIS,
016 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
017 * See the License for the specific language governing permissions and
018 * limitations under the License.
019 * ##########################################################################
020 */
021import java.net.DatagramSocket;
022import java.net.URI;
023import java.util.Arrays;
024import java.util.List;
025import java.util.Objects;
026import java.util.concurrent.ConcurrentSkipListMap;
027import java.util.concurrent.ScheduledFuture;
028import java.util.concurrent.TimeUnit;
029import java.util.regex.Pattern;
030import lombok.NoArgsConstructor;
031import lombok.ToString;
032import org.apache.hc.core5.http.Header;
033
034import static java.util.concurrent.TimeUnit.MILLISECONDS;
035import static java.util.concurrent.TimeUnit.MINUTES;
036import static java.util.concurrent.TimeUnit.SECONDS;
037
038/**
039 * SSDP discovery cache implementation.
040 *
041 * @author {@link.uri mailto:ball@hcf.dev Allen D. Ball}
042 */
043@NoArgsConstructor
044public class SSDPDiscoveryCache extends ConcurrentSkipListMap<URI,SSDPMessage> implements SSDPDiscoveryService.Listener {
045    private static final long serialVersionUID = 2743071044637511801L;
046
047    /** @serial */ private ScheduledFuture<?> expirer = null;
048    /** @serial */ private ScheduledFuture<?> msearch = null;
049    /** @serial */ private final List<SSDPDiscoveryService.Listener> listeners =
050        Arrays.asList(new NOTIFY(), new MSEARCH());
051
052    @Override
053    public void register(SSDPDiscoveryService service) {
054        if (expirer == null) {
055            expirer = service.scheduleAtFixedRate(() -> expirer(service), 0, 60, SECONDS);
056        }
057
058        if (msearch == null) {
059            msearch = service.scheduleAtFixedRate(() -> msearch(service), 0, 300, SECONDS);
060        }
061
062        listeners.stream().forEach(t -> service.addListener(t));
063    }
064
065    @Override
066    public void unregister(SSDPDiscoveryService service) {
067        ScheduledFuture<?> expirer = this.expirer;
068
069        if (expirer != null) {
070            expirer.cancel(true);
071        }
072
073        ScheduledFuture<?> msearch = this.msearch;
074
075        if (msearch != null) {
076            msearch.cancel(true);
077        }
078    }
079
080    @Override
081    public void sendEvent(SSDPDiscoveryService service, DatagramSocket socket, SSDPMessage message) {
082    }
083
084    @Override
085    public void receiveEvent(SSDPDiscoveryService service, DatagramSocket socket, SSDPMessage message) {
086    }
087
088    private void expirer(SSDPDiscoveryService service) {
089        long now = now();
090        boolean pending =
091            values().stream()
092            .mapToLong(t -> MINUTES.convert(t.getExpiration() - now, MILLISECONDS))
093            .anyMatch(t -> t <= expirer.getDelay(MINUTES));
094        boolean expired = values().removeIf(t -> t.getExpiration() < now);
095
096        if (expired || pending) {
097            service.submit(() -> msearch(service));
098        }
099    }
100
101    private void msearch(SSDPDiscoveryService service) {
102        service.msearch(15, SSDPMessage.SSDP_ALL);
103    }
104
105    private long now() { return System.currentTimeMillis(); }
106
107    private void update(URI usn, SSDPMessage message) {
108        if (usn != null) {
109            long time = now();
110
111            if (message.getExpiration() > time) {
112                put(usn, message);
113            }
114        }
115    }
116
117    @ToString
118    private class NOTIFY extends SSDPDiscoveryService.RequestHandler {
119        public NOTIFY() { super(SSDPRequest.Method.NOTIFY); }
120
121        @Override
122        public void run(SSDPDiscoveryService service, DatagramSocket socket, SSDPRequest request) {
123            String nts = request.getHeaderValue(SSDPMessage.NTS);
124
125            if (Objects.equals(SSDPMessage.SSDP_ALIVE, nts)) {
126                update(request.getUSN(), request);
127            } else if (Objects.equals(SSDPMessage.SSDP_UPDATE, nts)) {
128                /* update(request.getUSN(), request); */
129            } else if (Objects.equals(SSDPMessage.SSDP_BYEBYE, nts)) {
130                remove(request.getUSN());
131            }
132        }
133    }
134
135    @NoArgsConstructor @ToString
136    private class MSEARCH extends SSDPDiscoveryService.ResponseHandler {
137        @Override
138        public void run(SSDPDiscoveryService service, DatagramSocket socket, SSDPResponse response) {
139            update(response.getUSN(), response);
140        }
141    }
142}