001package ball.upnp.ssdp;
002/*-
003 * ##########################################################################
004 * UPnP/SSDP Implementation Classes
005 * $Id: SSDPRequest.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/ssdp/SSDPRequest.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 java.net.DatagramPacket;
024import java.net.InetSocketAddress;
025import java.net.SocketAddress;
026import java.net.URI;
027import java.util.List;
028import java.util.Objects;
029import java.util.function.Function;
030import java.util.regex.Pattern;
031import java.util.stream.Stream;
032import org.apache.http.Header;
033import org.apache.http.HttpVersion;
034import org.apache.http.message.BasicHttpRequest;
035
036import static java.lang.Math.max;
037import static java.lang.Math.min;
038import static java.util.stream.Collectors.joining;
039import static org.apache.commons.lang3.StringUtils.EMPTY;
040import static org.apache.commons.lang3.math.NumberUtils.toInt;
041import static org.apache.http.message.BasicLineParser.INSTANCE;
042import static org.apache.http.message.BasicLineParser.parseHeader;
043import static org.apache.http.message.BasicLineParser.parseRequestLine;
044
045/**
046 * SSDP {@link org.apache.http.HttpRequest} implementation.
047 *
048 * {@bean.info}
049 *
050 * @author {@link.uri mailto:ball@hcf.dev Allen D. Ball}
051 * @version $Revision: 7215 $
052 */
053public class SSDPRequest extends BasicHttpRequest implements SSDPMessage {
054
055    /**
056     * {@link SSDPRequest} enumerated {@link Method}s.
057     */
058    public enum Method {
059        NOTIFY(null), MSEARCH("M-SEARCH");
060
061        private final String string;
062
063        Method(String string) { this.string = string; }
064
065        @Override
066        public String toString() { return (string != null) ? string : name(); }
067
068        /**
069         * Test if the argument method matches {@link.this}.
070         *
071         * @param       method          The method to test.
072         *
073         * @return      {@code true} is match; {@code false} otherwise.
074         */
075        public boolean is(String method) {
076            return Objects.equals(toString(), method);
077        }
078    }
079
080    /**
081     * Method to parse a {@link SSDPRequest} from a {@link DatagramPacket}.
082     *
083     * @param   packet          The {@link DatagramPacket}.
084     *
085     * @return  A new {@link SSDPRequest}.
086     */
087    public static SSDPRequest from(DatagramPacket packet) {
088        return new SSDPRequest(packet);
089    }
090
091    private SocketAddress address = null;
092    private long timestamp = System.currentTimeMillis();
093    private Long expiration = null;
094
095    /**
096     * Sole non-private constructor.
097     *
098     * @param   method          The {@link SSDPRequest} {@link Method}.
099     */
100    protected SSDPRequest(Method method) {
101        super(method.toString(), "*", HttpVersion.HTTP_1_1);
102    }
103
104    private SSDPRequest(DatagramPacket packet) {
105        this(packet, SSDPMessage.parse(packet));
106    }
107
108    private SSDPRequest(DatagramPacket packet, List<String> lines) {
109        super(parseRequestLine(lines.remove(0), INSTANCE));
110
111        lines.stream().forEach(t -> addHeader(parseHeader(t, INSTANCE)));
112
113        address = packet.getSocketAddress();
114    }
115
116    /**
117     * Method to get the {@link org.apache.http.RequestLine} method.
118     *
119     * @return  The method specified on the request line.
120     */
121    public String getMethod() { return getRequestLine().getMethod(); }
122
123    /**
124     * Method to get the {@link SocketAddress} from the
125     * {@link DatagramPacket} if {@link.this} {@link SSDPRequest} was
126     * parsed from a packet.
127     *
128     * @return  The {@link SocketAddress}.
129     */
130    public SocketAddress getSocketAddress() { return address; }
131
132    /**
133     * Method to get the {@code MX} header value as an {@code int}.  Returns
134     * {@code 120} if the header is not specified or the if the value is not
135     * in the range of {@code 1 <= mx <= 120}.
136     *
137     * @return  The {@code MX} value.
138     */
139    public int getMX() {
140        return getHeaderValue(t -> min(max(toInt(t, 120), 1), 120), MX);
141    }
142
143    /**
144     * {@link String} fluent header setter.
145     *
146     * @param   name            The header name.
147     * @param   value           The header value.
148     *
149     * @return  {@link.this}
150     */
151    public SSDPRequest header(String name, String value) {
152        setHeader(name, value);
153
154        return this;
155    }
156
157    /**
158     * {@link SocketAddress} fluent header setter.
159     *
160     * @param   name            The header name.
161     * @param   value           The header value.
162     *
163     * @return  {@link.this}
164     */
165    public SSDPRequest header(String name, SocketAddress value) {
166        return header(name, (InetSocketAddress) value);
167    }
168
169    /**
170     * {@link InetSocketAddress} fluent header setter.
171     *
172     * @param   name            The header name.
173     * @param   value           The header value.
174     *
175     * @return  {@link.this}
176     */
177    public SSDPRequest header(String name, InetSocketAddress value) {
178        return header(name,
179                      t -> String.format("%s:%d",
180                                         value.getAddress().getHostAddress(),
181                                         value.getPort()),
182                      value);
183    }
184
185    /**
186     * {@link Number} fluent header setter.
187     *
188     * @param   name            The header name.
189     * @param   value           The header value.
190     *
191     * @return  {@link.this}
192     */
193    public SSDPRequest header(String name, Number value) {
194        return header(name, Number::toString, value);
195    }
196
197    /**
198     * {@link URI} fluent header setter.
199     *
200     * @param   name            The header name.
201     * @param   value           The header value.
202     *
203     * @return  {@link.this}
204     */
205    public SSDPRequest header(String name, URI value) {
206        return header(name, URI::toASCIIString, value);
207    }
208
209    /**
210     * Fluent header setter.
211     *
212     * @param   <T>             The target type.
213     * @param   name            The header name.
214     * @param   value           The header value.
215     *
216     * @return  {@link.this}
217     */
218    public <T> SSDPRequest header(String name,
219                                  Function<T,String> function, T value) {
220        setHeader(name, (value != null) ? function.apply(value) : null);
221
222        return this;
223    }
224
225    @Override
226    public long getExpiration() {
227        if (expiration == null) {
228            expiration = SSDPMessage.getExpiration(this, timestamp);
229        }
230
231        return expiration;
232    }
233
234    @Override
235    public String toString() {
236        String string =
237            Stream.concat(Stream.of(getRequestLine()),
238                          Stream.of(getAllHeaders()))
239            .filter(Objects::nonNull)
240            .map(Objects::toString)
241            .collect(joining(EOL, EMPTY, EOM));
242
243        return string;
244    }
245}