001package ball.upnp.ant.taskdefs; 002/*- 003 * ########################################################################## 004 * UPnP/SSDP Implementation Classes 005 * $Id: SSDPTask.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/ant/taskdefs/SSDPTask.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 ball.swing.table.ArrayListTableModel; 024import ball.swing.table.MapTableModel; 025import ball.upnp.SSDP; 026import ball.upnp.ssdp.SSDPDiscoveryCache; 027import ball.upnp.ssdp.SSDPDiscoveryService; 028import ball.upnp.ssdp.SSDPMessage; 029import ball.upnp.ssdp.SSDPRequest; 030import ball.upnp.ssdp.SSDPResponse; 031import ball.util.ant.taskdefs.AnnotatedAntTask; 032import ball.util.ant.taskdefs.AntTask; 033import ball.util.ant.taskdefs.ClasspathDelegateAntTask; 034import ball.util.ant.taskdefs.ConfigurableAntTask; 035import java.io.IOException; 036import java.io.InputStream; 037import java.net.DatagramSocket; 038import java.net.InetSocketAddress; 039import java.net.SocketAddress; 040import java.net.URI; 041import java.time.Duration; 042import java.util.Properties; 043import java.util.concurrent.ConcurrentSkipListMap; 044import lombok.Getter; 045import lombok.NoArgsConstructor; 046import lombok.NonNull; 047import lombok.Setter; 048import lombok.Synchronized; 049import lombok.ToString; 050import lombok.experimental.Accessors; 051import org.apache.http.HttpHeaders; 052import org.apache.tools.ant.BuildException; 053import org.apache.tools.ant.Task; 054import org.apache.tools.ant.util.ClasspathUtils; 055 056import static java.util.concurrent.TimeUnit.SECONDS; 057import static lombok.AccessLevel.PROTECTED; 058 059/** 060 * Abstract {@link.uri http://ant.apache.org/ Ant} {@link Task} base class 061 * for SSDP tasks. 062 * 063 * {@ant.task} 064 * 065 * @author {@link.uri mailto:ball@hcf.dev Allen D. Ball} 066 * @version $Revision: 7215 $ 067 */ 068@NoArgsConstructor(access = PROTECTED) 069public abstract class SSDPTask extends Task 070 implements AnnotatedAntTask, 071 ClasspathDelegateAntTask, 072 ConfigurableAntTask, 073 SSDPDiscoveryService.Listener { 074 private static final String VERSION; 075 076 static { 077 try { 078 Properties properties = new Properties(); 079 Class<?> type = SSDPTask.class; 080 String resource = type.getSimpleName() + ".properties.xml"; 081 082 try (InputStream in = type.getResourceAsStream(resource)) { 083 properties.loadFromXML(in); 084 } 085 086 VERSION = properties.getProperty("version"); 087 } catch (Exception exception) { 088 throw new ExceptionInInitializerError(exception); 089 } 090 } 091 092 @Getter @Setter @Accessors(chain = true, fluent = true) 093 private ClasspathUtils.Delegate delegate = null; 094 @Getter @Setter @NonNull 095 private String product = getAntTaskName() + "/" + VERSION; 096 097 @Override 098 public void init() throws BuildException { 099 super.init(); 100 ClasspathDelegateAntTask.super.init(); 101 ConfigurableAntTask.super.init(); 102 } 103 104 @Override 105 public void execute() throws BuildException { 106 super.execute(); 107 AnnotatedAntTask.super.execute(); 108 } 109 110 @Override 111 public void register(SSDPDiscoveryService service) { } 112 113 @Override 114 public void unregister(SSDPDiscoveryService service) { } 115 116 @Synchronized 117 @Override 118 public void sendEvent(SSDPDiscoveryService service, 119 DatagramSocket socket, SSDPMessage message) { 120 // log(toString(socket) + " --> " /* + message.getSocketAddress() */); 121 log("--- Outgoing ---"); 122 log(String.valueOf(message)); 123 } 124 125 @Synchronized 126 @Override 127 public void receiveEvent(SSDPDiscoveryService service, 128 DatagramSocket socket, SSDPMessage message) { 129 // log(toString(socket) + " <-- " /* + message.getSocketAddress() */); 130 log("--- Incoming ---"); 131 log(String.valueOf(message)); 132 } 133 134 private String toString(DatagramSocket socket) { 135 return toString(socket.getLocalSocketAddress()); 136 } 137 138 private String toString(SocketAddress address) { 139 return toString((InetSocketAddress) address); 140 } 141 142 private String toString(InetSocketAddress address) { 143 return String.format("%s:%d", 144 address.getAddress().getHostAddress(), 145 address.getPort()); 146 } 147 148 private class SSDPDiscoveryServiceImpl extends SSDPDiscoveryService { 149 public SSDPDiscoveryServiceImpl() throws IOException { 150 super(getProduct()); 151 } 152 } 153 154 /** 155 * {@link.uri http://ant.apache.org/ Ant} {@link Task} to run SSDP 156 * discovery. 157 * 158 * {@ant.task} 159 */ 160 @AntTask("ssdp-discover") 161 @NoArgsConstructor @ToString 162 public static class Discover extends SSDPTask { 163 @Getter @Setter 164 private int timeout = 180; 165 166 @Override 167 public void execute() throws BuildException { 168 super.execute(); 169 170 ClassLoader loader = 171 Thread.currentThread().getContextClassLoader(); 172 173 try { 174 Thread.currentThread().setContextClassLoader(getClassLoader()); 175 176 SSDPDiscoveryCache cache = new SSDPDiscoveryCache(); 177 SSDPDiscoveryService service = 178 new SSDPDiscoveryServiceImpl() 179 .addListener(this) 180 .addListener(cache); 181 182 service.awaitTermination(getTimeout(), SECONDS); 183 184 log(new TableModelImpl(cache)); 185 } catch (BuildException exception) { 186 throw exception; 187 } catch (Throwable throwable) { 188 throwable.printStackTrace(); 189 throw new BuildException(throwable); 190 } finally { 191 Thread.currentThread().setContextClassLoader(loader); 192 } 193 } 194 195 private class TableModelImpl extends ArrayListTableModel<SSDPMessage> { 196 private static final long serialVersionUID = 6644683980831866749L; 197 198 public TableModelImpl(SSDPDiscoveryCache cache) { 199 super(cache.values(), 200 SSDPMessage.USN, 201 HttpHeaders.EXPIRES, SSDPMessage.LOCATION); 202 } 203 204 @Override 205 protected Object getValueAt(SSDPMessage row, int x) { 206 Object object = null; 207 208 switch (x) { 209 default: 210 case 0: 211 object = row.getUSN(); 212 break; 213 214 case 1: 215 object = 216 Duration.ofMillis(row.getExpiration() 217 - System.currentTimeMillis()) 218 .toString(); 219 break; 220 221 case 2: 222 object = row.getLocation(); 223 break; 224 } 225 226 return object; 227 } 228 } 229 } 230 231 /** 232 * {@link.uri http://ant.apache.org/ Ant} {@link Task} listen on the 233 * SSDP UDP port. 234 * 235 * {@ant.task} 236 */ 237 @AntTask("ssdp-listen") 238 @NoArgsConstructor @ToString 239 public static class Listen extends SSDPTask { 240 @Override 241 public void execute() throws BuildException { 242 super.execute(); 243 244 ClassLoader loader = 245 Thread.currentThread().getContextClassLoader(); 246 247 try { 248 Thread.currentThread().setContextClassLoader(getClassLoader()); 249 250 SSDPDiscoveryService service = 251 new SSDPDiscoveryServiceImpl() 252 .addListener(this); 253 254 service.awaitTermination(Long.MAX_VALUE, SECONDS); 255 } catch (BuildException exception) { 256 throw exception; 257 } catch (Throwable throwable) { 258 throwable.printStackTrace(); 259 throw new BuildException(throwable); 260 } finally { 261 Thread.currentThread().setContextClassLoader(loader); 262 } 263 } 264 } 265 266 /** 267 * {@link.uri http://ant.apache.org/ Ant} {@link Task} to send an 268 * {@code M-SEARCH} command and then listen on the SSDP UDP port. 269 * 270 * {@ant.task} 271 */ 272 @AntTask("ssdp-m-search") 273 @NoArgsConstructor @ToString 274 public static class MSearch extends SSDPTask { 275 private static final ConcurrentSkipListMap<URI,URI> map = 276 new ConcurrentSkipListMap<>(); 277 @Getter @Setter 278 private int mx = 5; 279 @Getter @Setter 280 private URI st = SSDPMessage.SSDP_ALL; 281 282 @Override 283 public void execute() throws BuildException { 284 super.execute(); 285 286 ClassLoader loader = 287 Thread.currentThread().getContextClassLoader(); 288 289 try { 290 Thread.currentThread().setContextClassLoader(getClassLoader()); 291 292 SSDPDiscoveryService service = 293 new SSDPDiscoveryServiceImpl() 294 .addListener(this) 295 .addListener(new MSEARCH()); 296 297 service.msearch(getMx(), getSt()); 298 service.awaitTermination(getMx(), SECONDS); 299 300 log(new MapTableModel(map, 301 SSDPMessage.USN, SSDPMessage.LOCATION)); 302 } catch (BuildException exception) { 303 throw exception; 304 } catch (Throwable throwable) { 305 throwable.printStackTrace(); 306 throw new BuildException(throwable); 307 } finally { 308 Thread.currentThread().setContextClassLoader(loader); 309 } 310 } 311 312 @NoArgsConstructor @ToString 313 private class MSEARCH extends SSDPDiscoveryService.ResponseHandler { 314 @Override 315 public void run(SSDPDiscoveryService service, 316 DatagramSocket socket, SSDPResponse response) { 317 if (matches(getSt(), response.getST())) { 318 map.put(response.getUSN(), response.getLocation()); 319 } 320 } 321 322 private boolean matches(URI st, URI nt) { 323 return (SSDPMessage.SSDP_ALL.equals(st) 324 || st.toString().equalsIgnoreCase(nt.toString())); 325 } 326 } 327 } 328}