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}