001package ball.upnp.ssdp;
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 java.net.DatagramPacket;
022import java.net.URI;
023import java.util.Date;
024import java.util.List;
025import java.util.Objects;
026import java.util.function.Function;
027import java.util.regex.Pattern;
028import java.util.stream.Stream;
029import org.apache.hc.client5.http.utils.DateUtils;
030import org.apache.hc.core5.http.Header;
031import org.apache.hc.core5.http.HttpHeaders;
032import org.apache.hc.core5.http.HttpMessage;
033import org.apache.hc.core5.http.message.BasicHeaderValueParser;
034import org.apache.hc.core5.http.message.ParserCursor;
035import org.apache.hc.core5.util.CharArrayBuffer;
036
037import static java.util.concurrent.TimeUnit.MILLISECONDS;
038import static java.util.concurrent.TimeUnit.SECONDS;
039import static java.util.stream.Collectors.toList;
040
041/**
042 * SSDP {@link HttpMessage} interface definition.
043 *
044 * @author {@link.uri mailto:ball@hcf.dev Allen D. Ball}
045 */
046public interface SSDPMessage extends HttpMessage {
047
048    /**
049     * SSDP message header name.
050     */
051    public static final String
052        AL = "AL",
053        CACHE_CONTROL = HttpHeaders.CACHE_CONTROL.toUpperCase(),
054        DATE = HttpHeaders.DATE,
055        EXT = "EXT",
056        HOST = HttpHeaders.HOST.toUpperCase(),
057        LOCATION = HttpHeaders.LOCATION.toUpperCase(),
058        MAN = "MAN",
059        MX = "MX",
060        NT = "NT",
061        NTS = "NTS",
062        SERVER = HttpHeaders.SERVER.toUpperCase(),
063        ST = "ST",
064        USN = "USN",
065        BOOTID_UPNP_ORG = "BOOTID.UPNP.ORG",
066        NEXTBOOTID_UPNP_ORG = "NEXTBOOTID.UPNP.ORG",
067        CONFIGID_UPNP_ORG = "CONFIGID.UPNP.ORG",
068        SEARCHPORT_UPNP_ORG = "SEARCHPORT.UPNP.ORG",
069        USER_AGENT = "USER-AGENT",
070        SECURELOCATION_UPNP_ORG = "SECURELOCATION.UPNP.ORG";
071
072    /**
073     * SSDP {@link #NTS} value.
074     */
075    public static final String
076        SSDP_ALIVE = "ssdp:alive",
077        SSDP_UPDATE = "ssdp:update",
078        SSDP_BYEBYE = "ssdp:byebye";
079
080    /**
081     * SSDP {@link #ST} value.
082     */
083    public static final URI SSDP_ALL = URI.create("ssdp:all");
084
085    /**
086     * HTTP cache control key.
087     */
088    public static final String
089        MAX_AGE = "max-age";
090
091    /**
092     * {@link SSDPMessage} end-of-line sequence.
093     */
094    public static final String EOL = "\r\n";
095
096    /**
097     * {@link SSDPMessage} end-of-message sequence.
098     */
099    public static final String EOM = EOL + EOL;
100
101    /**
102     * Static method to parse a {@link DatagramPacket} to a {@link List} of
103     * lines ({@link CharArrayBuffer}s).
104     *
105     * @param   packet          The {@link DatagramPacket}.
106     *
107     * @return  The {@link List} of parsed lines as {@link CharArrayBuffer}s.
108     */
109    public static List<CharArrayBuffer> parse(DatagramPacket packet) {
110        CharArrayBuffer buffer = toCharArrayBuffer(packet);
111        List<CharArrayBuffer> list =
112            Pattern.compile(EOL).splitAsStream(buffer)
113            .map(SSDPMessage::toCharArrayBuffer)
114            .collect(toList());
115
116        return list;
117    }
118
119    /**
120     * Static method to convert a {@link DatagramPacket} to a
121     * {@link CharArrayBuffer}.
122     *
123     * @param   packet          The {@link DatagramPacket}.
124     *
125     * @return  The {@link CharArrayBuffer}.
126     */
127    public static CharArrayBuffer toCharArrayBuffer(DatagramPacket packet) {
128        CharArrayBuffer buffer = new CharArrayBuffer(packet.getLength());
129
130        buffer.append(packet.getData(), packet.getOffset(), packet.getLength());
131
132        return buffer;
133    }
134
135    /**
136     * Static method to convert a {@link String} to a {@link CharArrayBuffer}.
137     *
138     * @param   string          The {@link String}.
139     *
140     * @return  The {@link CharArrayBuffer}.
141     */
142    public static CharArrayBuffer toCharArrayBuffer(String string) {
143        CharArrayBuffer buffer = new CharArrayBuffer(string.length());
144
145        buffer.append(string);
146
147        return buffer;
148    }
149
150    /**
151     * Method to get the expiration time for {@link.this} {@link SSDPMessage}.
152     *
153     * @return  The expiration time (milliseconds since the UNIX epoch).
154     */
155    public long getExpiration();
156
157    /**
158     * Method to find the first {@link Header} matching {@code names} and
159     * return that value.
160     *
161     * @param   names           The candidate {@link Header} names.
162     *
163     * @return  The value or {@code null} if no header found.
164     */
165    default String getHeaderValue(String... names) {
166        String string =
167            Stream.of(names)
168            .map(this::getFirstHeader)
169            .filter(Objects::nonNull)
170            .map(Header::getValue)
171            .findFirst().orElse(null);
172
173        return string;
174    }
175
176    /**
177     * Method to find the first {@link Header} matching {@code names} and
178     * return the value converted with {@code function}.
179     *
180     * @param   <T>             The target type.
181     * @param   function        The conversion {@code Function}.
182     * @param   names           The candidate {@link Header} names.
183     *
184     * @return  The converted value or {@code null} if no header found.
185     */
186    default <T> T getHeaderValue(Function<String,T> function, String... names) {
187        String string = getHeaderValue(names);
188
189        return (string != null) ? function.apply(string) : null;
190    }
191
192    /**
193     * Method to find the first {@link Header} matching {@code name} with a
194     * parameter matching {@code parameter} and return that parameter value.
195     *
196     * @param   name            The target {@link Header} name.
197     * @param   parameter       The target parameter name.
198     *
199     * @return  The value or {@code null} if no header/parameter combination
200     *          is found.
201     */
202    default String getHeaderParameterValue(String name, String parameter) {
203        String value =
204            Stream.of(getHeaders(name))
205            .filter(Objects::nonNull)
206            .map(Header::getValue)
207            .map(t -> BasicHeaderValueParser.INSTANCE.parseElements(t, new ParserCursor(0, t.length())))
208            .filter(Objects::nonNull)
209            .flatMap(Stream::of)
210            .filter(t -> parameter.equalsIgnoreCase(t.getName()))
211            .map(t -> t.getValue())
212            .filter(Objects::nonNull)
213            .findFirst().orElse(null);
214
215        return value;
216    }
217
218    /**
219     * Method to find the first {@link Header} matching {@code name} with a
220     * parameter matching {@code parameter} and return that parameter value
221     * converted with {@code function}.
222     *
223     * @param   <T>             The target type.
224     * @param   name            The target {@link Header} name.
225     * @param   parameter       The target parameter name.
226     *
227     * @return  The value or {@code null} if no header/parameter combination
228     *          is found.
229     */
230    default <T> T getHeaderParameterValue(Function<String,T> function, String name, String parameter) {
231        String string = getHeaderParameterValue(name, parameter);
232
233        return (string != null) ? function.apply(string) : null;
234    }
235
236    /**
237     * Method to get the {@value #NT} {@link URI}.
238     *
239     * @return  The {@value #NT} {@link URI}.
240     */
241    default URI getNT() { return getHeaderValue(URI::create, NT); }
242
243    /**
244     * Method to get the {@value #ST} {@link URI}.
245     *
246     * @return  The {@value #ST} {@link URI}.
247     */
248    default URI getST() { return getHeaderValue(URI::create, ST); }
249
250    /**
251     * Method to get the {@value #USN} {@link URI}.
252     *
253     * @return  The {@value #USN} {@link URI}.
254     */
255    default URI getUSN() { return getHeaderValue(URI::create, USN); }
256
257    /**
258     * Method to get the location {@link URI}.
259     *
260     * @return  The service location {@link URI}.
261     */
262    default URI getLocation() {
263        return getHeaderValue(URI::create, LOCATION, AL);
264    }
265
266    /**
267     * Implementation method for {@link #getExpiration()}.
268     *
269     * @param   message         The {@link SSDPMessage}.
270     * @param   timestamp       The message's timestamp.
271     *
272     * @return  The expiration time (milliseconds since the UNIX epoch).
273     */
274    public static long getExpiration(SSDPMessage message, long timestamp) {
275        long expiration = timestamp;
276        Date date = message.getHeaderValue(DateUtils::parseDate, DATE);
277
278        if (date != null) {
279            expiration = date.getTime();
280        }
281
282        Long maxAge = message.getHeaderParameterValue(Long::decode, CACHE_CONTROL, MAX_AGE);
283
284        expiration += MILLISECONDS.convert((maxAge != null) ? maxAge : 0, SECONDS);
285
286        return expiration;
287    }
288}