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