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}