001package ball.upnp.ssdp;
002/*-
003 * ##########################################################################
004 * UPnP/SSDP Implementation Classes
005 * $Id: SSDPDiscoveryCache.java 5285 2020-02-05 04:23:21Z ball $
006 * $HeadURL: svn+ssh://svn.hcf.dev/var/spool/scm/repository.svn/ball-upnp/trunk/src/main/java/ball/upnp/ssdp/SSDPDiscoveryCache.java $
007 * %%
008 * Copyright (C) 2013 - 2020 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 java.net.URI;
024import java.util.Date;
025import java.util.TreeMap;
026import java.util.concurrent.ConcurrentSkipListMap;
027import java.util.regex.Pattern;
028import org.apache.http.Header;
029import org.apache.http.HttpHeaders;
030import org.apache.http.client.utils.DateUtils;
031
032import static ball.upnp.ssdp.SSDPMessage.MAX_AGE;
033import static ball.upnp.ssdp.SSDPMessage.SSDP_BYEBYE;
034import static java.util.Objects.requireNonNull;
035
036/**
037 * SSDP discovery cache implementation.
038 *
039 * @author {@link.uri mailto:ball@hcf.dev Allen D. Ball}
040 * @version $Revision: 5285 $
041 */
042public class SSDPDiscoveryCache
043             extends ConcurrentSkipListMap<URI,SSDPDiscoveryCache.Value>
044             implements SSDPDiscoveryThread.Listener {
045    private static final long serialVersionUID = -383765398333867476L;
046
047    /**
048     * Sole constructor.
049     */
050    public SSDPDiscoveryCache() {
051        super();
052
053        new Thread() {
054            { setDaemon(true); }
055
056            @Override
057            public void run() {
058                for (;;) {
059                    try {
060                        sleep(60 * 1000);
061                    } catch (InterruptedException exception) {
062                    }
063
064                    values().removeIf(t -> now() > t.getExpiration());
065                }
066            }
067        }.start();
068    }
069
070    @Override
071    public void sendEvent(SSDPDiscoveryThread thread, SSDPMessage message) {
072        receiveEvent(thread, message);
073    }
074
075    @Override
076    public void receiveEvent(SSDPDiscoveryThread thread, SSDPMessage message) {
077        try {
078            long time = now();
079            long expiration = 0;
080            Header header = message.getFirstHeader(HttpHeaders.CACHE_CONTROL);
081
082            if (header != null) {
083                CacheControlDirectiveMap map =
084                    new CacheControlDirectiveMap(header.getValue());
085                String value = map.get(MAX_AGE);
086
087                if (value != null) {
088                    try {
089                        header = message.getFirstHeader(HttpHeaders.DATE);
090
091                        if (header != null) {
092                            time =
093                                DateUtils.parseDate(header.getValue())
094                                .getTime();
095                        }
096                    } catch (Exception exception) {
097                    }
098
099                    expiration = time + (Long.decode(value) * 1000);
100                }
101            } else {
102                header = message.getFirstHeader(HttpHeaders.EXPIRES);
103
104                if (header != null) {
105                    String value = header.getValue();
106                    Date date = DateUtils.parseDate(value);
107
108                    if (date != null) {
109                        expiration = date.getTime();
110                    }
111                }
112            }
113
114            if (expiration > time) {
115                put(message.getUSN(), new Value(message, expiration));
116            }
117        } catch (Exception exception) {
118        }
119
120        if (message instanceof SSDPRequest) {
121            SSDPRequest request = (SSDPRequest) message;
122            String method = request.getRequestLine().getMethod();
123
124            if (SSDPNotifyRequest.METHOD.equals(method)) {
125                Header header = message.getFirstHeader(SSDPMessage.NTS);
126
127                if (header != null && SSDP_BYEBYE.equals(header.getValue())) {
128                    remove(request.getUSN());
129                }
130            }
131        }
132    }
133
134    private long now() { return System.currentTimeMillis(); }
135
136    /**
137     * {@link SSDPDiscoveryCache} {@link java.util.Map} {@link Value}
138     * (expiration and {@link SSDPMessage}).
139     *
140     * {@bean.info}
141     */
142    public class Value {
143        private final SSDPMessage message;
144        private long expiration = 0;
145
146        private Value(SSDPMessage message, long expiration) {
147            this.message = requireNonNull(message, "message");
148
149            setExpiration(expiration);
150        }
151
152        public SSDPMessage getSSDPMessage() { return message; }
153
154        public long getExpiration() { return expiration; }
155        public void setExpiration(long expiration) {
156            if (expiration > 0) {
157                this.expiration = expiration;
158            } else {
159                throw new IllegalArgumentException("expiration=" + expiration);
160            }
161        }
162
163        @Override
164        public String toString() { return message.toString(); }
165    }
166
167    private class CacheControlDirectiveMap extends TreeMap<String,String> {
168        private static final long serialVersionUID = -7901522510091761313L;
169
170        public CacheControlDirectiveMap(String string) {
171            super(String.CASE_INSENSITIVE_ORDER);
172
173            for (String directive : string.trim().split(Pattern.quote(";"))) {
174                String[] pair = directive.split(Pattern.quote("="), 2);
175
176                put(pair[0].trim(), (pair.length > 1) ? pair[1].trim() : null);
177            }
178        }
179    }
180}