View Javadoc
1   /*
2    * Copyright 2020 The Netty Project
3    *
4    * The Netty Project licenses this file to you under the Apache License,
5    * version 2.0 (the "License"); you may not use this file except in compliance
6    * with the License. You may obtain a copy of the License at:
7    *
8    *   https://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13   * License for the specific language governing permissions and limitations
14   * under the License.
15   */
16  package io.netty.handler.codec.mqtt;
17  
18  import io.netty.util.collection.IntObjectHashMap;
19  
20  import java.util.Collection;
21  import java.util.Collections;
22  import java.util.List;
23  import java.util.ArrayList;
24  
25  /**
26   * MQTT Properties container
27   * */
28  public final class MqttProperties {
29  
30      public enum MqttPropertyType {
31          // single byte properties
32          PAYLOAD_FORMAT_INDICATOR(0x01),
33          REQUEST_PROBLEM_INFORMATION(0x17),
34          REQUEST_RESPONSE_INFORMATION(0x19),
35          MAXIMUM_QOS(0x24),
36          RETAIN_AVAILABLE(0x25),
37          WILDCARD_SUBSCRIPTION_AVAILABLE(0x28),
38          SUBSCRIPTION_IDENTIFIER_AVAILABLE(0x29),
39          SHARED_SUBSCRIPTION_AVAILABLE(0x2A),
40  
41          // two bytes properties
42          SERVER_KEEP_ALIVE(0x13),
43          RECEIVE_MAXIMUM(0x21),
44          TOPIC_ALIAS_MAXIMUM(0x22),
45          TOPIC_ALIAS(0x23),
46  
47          // four bytes properties
48          PUBLICATION_EXPIRY_INTERVAL(0x02),
49          SESSION_EXPIRY_INTERVAL(0x11),
50          WILL_DELAY_INTERVAL(0x18),
51          MAXIMUM_PACKET_SIZE(0x27),
52  
53          // Variable Byte Integer
54          SUBSCRIPTION_IDENTIFIER(0x0B),
55  
56          // UTF-8 Encoded String properties
57          CONTENT_TYPE(0x03),
58          RESPONSE_TOPIC(0x08),
59          ASSIGNED_CLIENT_IDENTIFIER(0x12),
60          AUTHENTICATION_METHOD(0x15),
61          RESPONSE_INFORMATION(0x1A),
62          SERVER_REFERENCE(0x1C),
63          REASON_STRING(0x1F),
64          USER_PROPERTY(0x26),
65  
66          // Binary Data
67          CORRELATION_DATA(0x09),
68          AUTHENTICATION_DATA(0x16);
69  
70          private static final MqttPropertyType[] VALUES;
71  
72          static {
73              VALUES = new MqttPropertyType[43];
74              for (MqttPropertyType v : values()) {
75                  VALUES[v.value] = v;
76              }
77          }
78  
79          private final int value;
80  
81          MqttPropertyType(int value) {
82              this.value = value;
83          }
84  
85          public int value() {
86              return value;
87          }
88  
89          public static MqttPropertyType valueOf(int type) {
90              MqttPropertyType t = null;
91              try {
92                  t = VALUES[type];
93              } catch (ArrayIndexOutOfBoundsException ignored) {
94                  // nop
95              }
96              if (t == null) {
97                  throw new IllegalArgumentException("unknown property type: " + type);
98              }
99              return t;
100         }
101     }
102 
103     public static final MqttProperties NO_PROPERTIES = new MqttProperties(false);
104 
105     static MqttProperties withEmptyDefaults(MqttProperties properties) {
106         if (properties == null) {
107             return MqttProperties.NO_PROPERTIES;
108         }
109         return properties;
110     }
111 
112     /**
113      * MQTT property base class
114      *
115      * @param <T> property type
116      */
117     public abstract static class MqttProperty<T> {
118         final T value;
119         final int propertyId;
120 
121         protected MqttProperty(int propertyId, T value) {
122             this.propertyId = propertyId;
123             this.value = value;
124         }
125 
126         /**
127          * Get MQTT property value
128          *
129          * @return property value
130          */
131         public T value() {
132             return value;
133         }
134 
135         /**
136          * Get MQTT property ID
137          * @return property ID
138          */
139         public int propertyId() {
140             return propertyId;
141         }
142 
143         @Override
144         public int hashCode() {
145             return propertyId + 31 * value.hashCode();
146         }
147 
148         @Override
149         public boolean equals(Object obj) {
150             if (this == obj) {
151                 return true;
152             }
153             if (obj == null || getClass() != obj.getClass()) {
154                 return false;
155             }
156             MqttProperty that = (MqttProperty) obj;
157             return this.propertyId == that.propertyId && this.value.equals(that.value);
158         }
159     }
160 
161     public static final class IntegerProperty extends MqttProperty<Integer> {
162 
163         public IntegerProperty(int propertyId, Integer value) {
164             super(propertyId, value);
165         }
166 
167         @Override
168         public String toString() {
169             return "IntegerProperty(" + propertyId + ", " + value + ")";
170         }
171     }
172 
173     public static final class StringProperty extends MqttProperty<String> {
174 
175         public StringProperty(int propertyId, String value) {
176             super(propertyId, value);
177         }
178 
179         @Override
180         public String toString() {
181             return "StringProperty(" + propertyId + ", " + value + ")";
182         }
183     }
184 
185     public static final class StringPair {
186         public final String key;
187         public final String value;
188 
189         public StringPair(String key, String value) {
190             this.key = key;
191             this.value = value;
192         }
193 
194         @Override
195         public int hashCode() {
196             return key.hashCode() + 31 * value.hashCode();
197         }
198 
199         @Override
200         public boolean equals(Object obj) {
201             if (this == obj) {
202                 return true;
203             }
204             if (obj == null || getClass() != obj.getClass()) {
205                 return false;
206             }
207             StringPair that = (StringPair) obj;
208 
209             return that.key.equals(this.key) && that.value.equals(this.value);
210         }
211     }
212 
213     //User properties are the only properties that may be included multiple times and
214     //are the only properties where ordering is required. Therefore, they need a special handling
215     public static final class UserProperties extends MqttProperty<List<StringPair>> {
216         public UserProperties() {
217             super(MqttPropertyType.USER_PROPERTY.value, new ArrayList<StringPair>());
218         }
219 
220         /**
221          * Create user properties from the collection of the String pair values
222          *
223          * @param values string pairs. Collection entries are copied, collection itself isn't shared
224          */
225         public UserProperties(Collection<StringPair> values) {
226             this();
227             this.value.addAll(values);
228         }
229 
230         private static UserProperties fromUserPropertyCollection(Collection<UserProperty> properties) {
231             UserProperties userProperties = new UserProperties();
232             for (UserProperty property: properties) {
233                 userProperties.add(new StringPair(property.value.key, property.value.value));
234             }
235             return userProperties;
236         }
237 
238         public void add(StringPair pair) {
239             this.value.add(pair);
240         }
241 
242         public void add(String key, String value) {
243             this.value.add(new StringPair(key, value));
244         }
245 
246         @Override
247         public String toString() {
248             StringBuilder builder = new StringBuilder("UserProperties(");
249             boolean first = true;
250             for (StringPair pair: value) {
251                 if (!first) {
252                     builder.append(", ");
253                 }
254                 builder.append(pair.key + "->" + pair.value);
255                 first = false;
256             }
257             builder.append(")");
258             return builder.toString();
259         }
260     }
261 
262     public static final class UserProperty extends MqttProperty<StringPair> {
263         public UserProperty(String key, String value) {
264             super(MqttPropertyType.USER_PROPERTY.value, new StringPair(key, value));
265         }
266 
267         @Override
268         public String toString() {
269             return "UserProperty(" + value.key + ", " + value.value + ")";
270         }
271     }
272 
273     public static final class BinaryProperty extends MqttProperty<byte[]> {
274 
275         public BinaryProperty(int propertyId, byte[] value) {
276             super(propertyId, value);
277         }
278 
279         @Override
280         public String toString() {
281             return "BinaryProperty(" + propertyId + ", " + value.length + " bytes)";
282         }
283     }
284 
285     public MqttProperties() {
286         this(true);
287     }
288 
289     private MqttProperties(boolean canModify) {
290         this.canModify = canModify;
291     }
292 
293     private IntObjectHashMap<MqttProperty> props;
294     private List<UserProperty> userProperties;
295     private List<IntegerProperty> subscriptionIds;
296     private final boolean canModify;
297 
298     public void add(MqttProperty property) {
299         if (!canModify) {
300             throw new UnsupportedOperationException("adding property isn't allowed");
301         }
302         IntObjectHashMap<MqttProperty> props = this.props;
303         if (property.propertyId == MqttPropertyType.USER_PROPERTY.value) {
304             List<UserProperty> userProperties = this.userProperties;
305             if (userProperties == null) {
306                 userProperties = new ArrayList<UserProperty>(1);
307                 this.userProperties = userProperties;
308             }
309             if (property instanceof UserProperty) {
310                 userProperties.add((UserProperty) property);
311             } else if (property instanceof UserProperties) {
312                 for (StringPair pair: ((UserProperties) property).value) {
313                     userProperties.add(new UserProperty(pair.key, pair.value));
314                 }
315             } else {
316                 throw new IllegalArgumentException("User property must be of UserProperty or UserProperties type");
317             }
318         } else if (property.propertyId == MqttPropertyType.SUBSCRIPTION_IDENTIFIER.value) {
319             List<IntegerProperty> subscriptionIds = this.subscriptionIds;
320             if (subscriptionIds == null) {
321                 subscriptionIds = new ArrayList<IntegerProperty>(1);
322                 this.subscriptionIds = subscriptionIds;
323             }
324             if (property instanceof IntegerProperty) {
325                 subscriptionIds.add((IntegerProperty) property);
326             } else {
327                 throw new IllegalArgumentException("Subscription ID must be an integer property");
328             }
329         } else {
330             if (props == null) {
331                 props = new IntObjectHashMap<MqttProperty>();
332                 this.props = props;
333             }
334             props.put(property.propertyId, property);
335         }
336     }
337 
338     public Collection<? extends MqttProperty> listAll() {
339         IntObjectHashMap<MqttProperty> props = this.props;
340         if (props == null && subscriptionIds == null && userProperties == null) {
341             return Collections.<MqttProperty>emptyList();
342         }
343         if (subscriptionIds == null && userProperties == null) {
344             return props.values();
345         }
346         if (props == null && userProperties == null) {
347             return subscriptionIds;
348         }
349         List<MqttProperty> propValues = new ArrayList<MqttProperty>(props != null ? props.size() : 1);
350         if (props != null) {
351             propValues.addAll(props.values());
352         }
353         if (subscriptionIds != null) {
354             propValues.addAll(subscriptionIds);
355         }
356         if (userProperties != null) {
357             propValues.add(UserProperties.fromUserPropertyCollection(userProperties));
358         }
359         return propValues;
360     }
361 
362     public boolean isEmpty() {
363         IntObjectHashMap<MqttProperty> props = this.props;
364         return props == null || props.isEmpty();
365     }
366 
367     /**
368      * Get property by ID. If there are multiple properties of this type (can be with Subscription ID)
369      * then return the first one.
370      *
371      * @param propertyId ID of the property
372      * @return a property if it is set, null otherwise
373      */
374     public MqttProperty getProperty(int propertyId) {
375         if (propertyId == MqttPropertyType.USER_PROPERTY.value) {
376             //special handling to keep compatibility with earlier versions
377             List<UserProperty> userProperties = this.userProperties;
378             if (userProperties == null) {
379                 return null;
380             }
381             return UserProperties.fromUserPropertyCollection(userProperties);
382         }
383         if (propertyId == MqttPropertyType.SUBSCRIPTION_IDENTIFIER.value) {
384             List<IntegerProperty> subscriptionIds = this.subscriptionIds;
385             if (subscriptionIds == null || subscriptionIds.isEmpty()) {
386                 return null;
387             }
388             return subscriptionIds.get(0);
389         }
390         IntObjectHashMap<MqttProperty> props = this.props;
391         return props == null ? null : props.get(propertyId);
392     }
393 
394     /**
395      * Get properties by ID.
396      * Some properties (Subscription ID and User Properties) may occur multiple times,
397      * this method returns all their values in order.
398      *
399      * @param propertyId ID of the property
400      * @return all properties having specified ID
401      */
402     public List<? extends MqttProperty> getProperties(int propertyId) {
403         if (propertyId == MqttPropertyType.USER_PROPERTY.value) {
404             return userProperties == null ? Collections.<MqttProperty>emptyList() : userProperties;
405         }
406         if (propertyId == MqttPropertyType.SUBSCRIPTION_IDENTIFIER.value) {
407             return subscriptionIds == null ? Collections.<MqttProperty>emptyList() : subscriptionIds;
408         }
409         IntObjectHashMap<MqttProperty> props = this.props;
410         return (props == null || !props.containsKey(propertyId)) ?
411                 Collections.<MqttProperty>emptyList() :
412                 Collections.singletonList(props.get(propertyId));
413     }
414 }