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