001/*
002 * Copyright (C) 2015-2016 Neo Visionaries Inc.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package NeoVisionaries.WebSockets;
017
018
019import java.util.LinkedHashMap;
020import java.util.Map;
021
022
023/**
024 * A class to hold the name and the parameters of
025 * a WebSocket extension.
026 * 
027 * <EMBED CLASS='external-html' DATA-FILE-ID=LICENSE>
028 */
029public class WebSocketExtension
030{
031    /**
032     * The name of <code>permessage-deflate</code> extension that is
033     * defined in <a href="https://tools.ietf.org/html/rfc7692#section-7"
034     * >7&#46; The "permessage-deflate" Extension</a> in <a href=
035     * "https://tools.ietf.org/html/rfc7692">RFC 7692</a>.
036     *
037     * @since 1.17
038     */
039    public static final String PERMESSAGE_DEFLATE = "permessage-deflate";
040
041
042    private final String mName;
043    private final Map<String, String> mParameters;
044
045
046    /**
047     * Constructor with an extension name.
048     *
049     * @param name
050     *         The extension name.
051     *
052     * @throws IllegalArgumentException
053     *         The given name is not a valid token.
054     */
055    public WebSocketExtension(String name)
056    {
057        // Check the validity of the name.
058        if (Token.isValid(name) == false)
059        {
060            // The name is not a valid token.
061            throw new IllegalArgumentException("'name' is not a valid token.");
062        }
063
064        mName       = name;
065        mParameters = new LinkedHashMap<String, String>();
066    }
067
068
069    /**
070     * Copy constructor.
071     *
072     * @param source
073     *         A source extension. Must not be {@code null}.
074     *
075     * @throws IllegalArgumentException
076     *         The given argument is {@code null}.
077     *
078     * @since 1.6
079     */
080    public WebSocketExtension(WebSocketExtension source)
081    {
082        if (source == null)
083        {
084            // If the given instance is null.
085            throw new IllegalArgumentException("'source' is null.");
086        }
087
088        mName       = source.getName();
089        mParameters = new LinkedHashMap<String, String>(source.getParameters());
090    }
091
092
093    /**
094     * Get the extension name.
095     *
096     * @return
097     *         The extension name.
098     */
099    public String getName()
100    {
101        return mName;
102    }
103
104
105    /**
106     * Get the parameters.
107     *
108     * @return
109     *         The parameters.
110     */
111    public Map<String, String> getParameters()
112    {
113        return mParameters;
114    }
115
116
117    /**
118     * Check if the parameter identified by the key is contained.
119     *
120     * @param key
121     *         The name of the parameter.
122     *
123     * @return
124     *         {@code true} if the parameter is contained.
125     */
126    public boolean containsParameter(String key)
127    {
128        return mParameters.containsKey(key);
129    }
130
131
132    /**
133     * Get the value of the specified parameter.
134     *
135     * @param key
136     *         The name of the parameter.
137     *
138     * @return
139     *         The value of the parameter. {@code null} may be returned.
140     */
141    public String getParameter(String key)
142    {
143        return mParameters.get(key);
144    }
145
146
147    /**
148     * Set a value to the specified parameter.
149     *
150     * @param key
151     *         The name of the parameter.
152     *
153     * @param value
154     *         The value of the parameter. If not {@code null}, it must be
155     *         a valid token. Note that <a href="http://tools.ietf.org/html/rfc6455"
156     *         >RFC 6455</a> says "<i>When using the quoted-string syntax
157     *         variant, the value after quoted-string unescaping MUST
158     *         conform to the 'token' ABNF.</i>"
159     *
160     * @return
161     *         {@code this} object.
162     *
163     * @throws IllegalArgumentException
164     *         <ul>
165     *         <li>The key is not a valid token.</li>
166     *         <li>The value is not {@code null} and it is not a valid token.</li>
167     *         </ul>
168     */
169    public WebSocketExtension setParameter(String key, String value)
170    {
171        // Check the validity of the key.
172        if (Token.isValid(key) == false)
173        {
174            // The key is not a valid token.
175            throw new IllegalArgumentException("'key' is not a valid token.");
176        }
177
178        // If the value is not null.
179        if (value != null)
180        {
181            // Check the validity of the value.
182            if (Token.isValid(value) == false)
183            {
184                // The value is not a valid token.
185                throw new IllegalArgumentException("'value' is not a valid token.");
186            }
187        }
188
189        mParameters.put(key, value);
190
191        return this;
192    }
193
194
195    /**
196     * Stringify this object into the format "{name}[; {key}[={value}]]*".
197     */
198    @Override
199    public String toString()
200    {
201        StringBuilder builder = new StringBuilder(mName);
202
203        for (Map.Entry<String, String> entry : mParameters.entrySet())
204        {
205            // "; {key}"
206            builder.append("; ").append(entry.getKey());
207
208            String value = entry.getValue();
209
210            if (value != null && value.length() != 0)
211            {
212                // "={value}"
213                builder.append("=").append(value);
214            }
215        }
216
217        return builder.toString();
218    }
219
220
221    /**
222     * Validate this instance. This method is expected to be overridden.
223     */
224    void validate() throws WebSocketException
225    {
226    }
227
228
229    /**
230     * Parse a string as a {@link WebSocketExtension}. The input string
231     * should comply with the format described in <a href=
232     * "https://tools.ietf.org/html/rfc6455#section-9.1">9.1. Negotiating
233     * Extensions</a> in <a href="https://tools.ietf.org/html/rfc6455"
234     * >RFC 6455</a>.
235     *
236     * @param string
237     *         A string that represents a WebSocket extension.
238     *
239     * @return
240     *         A new {@link WebSocketExtension} instance that represents
241     *         the given string. If the input string does not comply with
242     *         RFC 6455, {@code null} is returned.
243     */
244    public static WebSocketExtension parse(String string)
245    {
246        if (string == null)
247        {
248            return null;
249        }
250
251        // Split the string by semi-colons.
252        String[] elements = string.trim().split("\\s*;\\s*");
253
254        if (elements.length == 0)
255        {
256            // Even an extension name is not included.
257            return null;
258        }
259
260        // The first element is the extension name.
261        String name = elements[0];
262
263        if (Token.isValid(name) == false)
264        {
265            // The extension name is not a valid token.
266            return null;
267        }
268
269        // Create an instance for the extension name.
270        WebSocketExtension extension = createInstance(name);
271
272        // For each "{key}[={value}]".
273        for (int i = 1; i < elements.length; ++i)
274        {
275            // Split by '=' to get the key and the value.
276            String[] pair = elements[i].split("\\s*=\\s*", 2);
277
278            // If {key} is not contained.
279            if (pair.length == 0 || pair[0].length() == 0)
280            {
281                // Ignore.
282                continue;
283            }
284
285            // The name of the parameter.
286            String key = pair[0];
287
288            if (Token.isValid(key) == false)
289            {
290                // The parameter name is not a valid token.
291                // Ignore this parameter.
292                continue;
293            }
294
295            // The value of the parameter.
296            String value = extractValue(pair);
297
298            if (value != null)
299            {
300                if (Token.isValid(value) == false)
301                {
302                    // The parameter value is not a valid token.
303                    // Ignore this parameter.
304                    continue;
305                }
306            }
307
308            // Add the pair of the key and the value.
309            extension.setParameter(key, value);
310        }
311
312        return extension;
313    }
314
315
316    private static String extractValue(String[] pair)
317    {
318        if (pair.length != 2)
319        {
320            return null;
321        }
322
323        return Token.unquote(pair[1]);
324    }
325
326
327    private static WebSocketExtension createInstance(String name)
328    {
329        if (PERMESSAGE_DEFLATE.equals(name))
330        {
331            return new PerMessageDeflateExtension(name);
332        }
333
334        return new WebSocketExtension(name);
335    }
336}