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