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