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}