View Javadoc
1   /*
2    * Copyright (C) 2013 4th Line GmbH, Switzerland
3    *
4    * The contents of this file are subject to the terms of either the GNU
5    * Lesser General Public License Version 2 or later ("LGPL") or the
6    * Common Development and Distribution License Version 1 or later
7    * ("CDDL") (collectively, the "License"). You may not use this file
8    * except in compliance with the License. See LICENSE.txt for more
9    * information.
10   *
11   * This program is distributed in the hope that it will be useful,
12   * but WITHOUT ANY WARRANTY; without even the implied warranty of
13   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
14   */
15  
16  package org.fourthline.cling.binding.annotations;
17  
18  import org.fourthline.cling.binding.AllowedValueProvider;
19  import org.fourthline.cling.binding.AllowedValueRangeProvider;
20  import org.fourthline.cling.binding.LocalServiceBindingException;
21  import org.fourthline.cling.model.ModelUtil;
22  import org.fourthline.cling.model.meta.StateVariable;
23  import org.fourthline.cling.model.meta.StateVariableAllowedValueRange;
24  import org.fourthline.cling.model.meta.StateVariableEventDetails;
25  import org.fourthline.cling.model.meta.StateVariableTypeDetails;
26  import org.fourthline.cling.model.state.StateVariableAccessor;
27  import org.fourthline.cling.model.types.Datatype;
28  
29  import java.util.Set;
30  import java.util.logging.Logger;
31  
32  /**
33   * @author Christian Bauer
34   */
35  public class AnnotationStateVariableBinder {
36  
37      private static Logger log = Logger.getLogger(AnnotationLocalServiceBinder.class.getName());
38  
39      protected UpnpStateVariable annotation;
40      protected String name;
41      protected StateVariableAccessor accessor;
42      protected Set<Class> stringConvertibleTypes;
43  
44      public AnnotationStateVariableBinder(UpnpStateVariable annotation, String name,
45                                           StateVariableAccessor accessor, Set<Class> stringConvertibleTypes) {
46          this.annotation = annotation;
47          this.name = name;
48          this.accessor = accessor;
49          this.stringConvertibleTypes = stringConvertibleTypes;
50      }
51  
52      public UpnpStateVariable getAnnotation() {
53          return annotation;
54      }
55  
56      public String getName() {
57          return name;
58      }
59  
60      public StateVariableAccessor getAccessor() {
61          return accessor;
62      }
63  
64      public Set<Class> getStringConvertibleTypes() {
65          return stringConvertibleTypes;
66      }
67  
68      protected StateVariable createStateVariable() throws LocalServiceBindingException {
69  
70          log.fine("Creating state variable '" + getName() + "' with accessor: " + getAccessor());
71  
72          // Datatype
73          Datatype datatype = createDatatype();
74  
75          // Default value
76          String defaultValue = createDefaultValue(datatype);
77  
78          // Allowed values
79          String[] allowedValues = null;
80          if (Datatype.Builtin.STRING.equals(datatype.getBuiltin())) {
81  
82              if (getAnnotation().allowedValueProvider() != void.class) {
83                  allowedValues = getAllowedValuesFromProvider();
84              } else if (getAnnotation().allowedValues().length > 0) {
85                  allowedValues = getAnnotation().allowedValues();
86              } else if (getAnnotation().allowedValuesEnum() != void.class) {
87                  allowedValues = getAllowedValues(getAnnotation().allowedValuesEnum());
88              } else if (getAccessor() != null && getAccessor().getReturnType().isEnum()) {
89                  allowedValues = getAllowedValues(getAccessor().getReturnType());
90              } else {
91                  log.finer("Not restricting allowed values (of string typed state var): " + getName());
92              }
93  
94              if (allowedValues != null && defaultValue != null) {
95  
96                  // Check if the default value is an allowed value
97                  boolean foundValue = false;
98                  for (String s : allowedValues) {
99                      if (s.equals(defaultValue)) {
100                         foundValue = true;
101                         break;
102                     }
103                 }
104                 if (!foundValue) {
105                     throw new LocalServiceBindingException(
106                             "Default value '" + defaultValue + "' is not in allowed values of: " + getName()
107                     );
108                 }
109             }
110         }
111 
112         // Allowed value range
113         StateVariableAllowedValueRange allowedValueRange = null;
114         if (Datatype.Builtin.isNumeric(datatype.getBuiltin())) {
115 
116             if (getAnnotation().allowedValueRangeProvider() != void.class) {
117                 allowedValueRange = getAllowedRangeFromProvider();
118             } else if (getAnnotation().allowedValueMinimum() > 0 || getAnnotation().allowedValueMaximum() > 0) {
119                 allowedValueRange = getAllowedValueRange(
120                     getAnnotation().allowedValueMinimum(),
121                     getAnnotation().allowedValueMaximum(),
122                     getAnnotation().allowedValueStep()
123                 );
124             } else {
125                 log.finer("Not restricting allowed value range (of numeric typed state var): " + getName());
126             }
127 
128             // Check if the default value is an allowed value
129             if (defaultValue != null && allowedValueRange != null) {
130 
131                 long v;
132                 try {
133                     v = Long.valueOf(defaultValue);
134                 } catch (Exception ex) {
135                     throw new LocalServiceBindingException(
136                         "Default value '" + defaultValue + "' is not numeric (for range checking) of: " + getName()
137                     );
138                 }
139 
140                 if (!allowedValueRange.isInRange(v)) {
141                     throw new LocalServiceBindingException(
142                         "Default value '" + defaultValue + "' is not in allowed range of: " + getName()
143                     );
144                 }
145             }
146         }
147 
148         // Event details
149         boolean sendEvents = getAnnotation().sendEvents();
150         if (sendEvents && getAccessor() == null) {
151             throw new LocalServiceBindingException(
152                     "State variable sends events but has no accessor for field or getter: " + getName()
153             );
154         }
155 
156         int eventMaximumRateMillis = 0;
157         int eventMinimumDelta = 0;
158         if (sendEvents) {
159             if (getAnnotation().eventMaximumRateMilliseconds() > 0) {
160                 log.finer("Moderating state variable events using maximum rate (milliseconds): " + getAnnotation().eventMaximumRateMilliseconds());
161                 eventMaximumRateMillis = getAnnotation().eventMaximumRateMilliseconds();
162             }
163 
164             if (getAnnotation().eventMinimumDelta() > 0 && Datatype.Builtin.isNumeric(datatype.getBuiltin())) {
165                 // TODO: Doesn't consider floating point types!
166                 log.finer("Moderating state variable events using minimum delta: " + getAnnotation().eventMinimumDelta());
167                 eventMinimumDelta = getAnnotation().eventMinimumDelta();
168             }
169         }
170 
171         StateVariableTypeDetails typeDetails =
172                 new StateVariableTypeDetails(datatype, defaultValue, allowedValues, allowedValueRange);
173 
174         StateVariableEventDetails eventDetails =
175                 new StateVariableEventDetails(sendEvents, eventMaximumRateMillis, eventMinimumDelta);
176 
177         return new StateVariable(getName(), typeDetails, eventDetails);
178     }
179 
180     protected Datatype createDatatype() throws LocalServiceBindingException {
181 
182         String declaredDatatype = getAnnotation().datatype();
183 
184         if (declaredDatatype.length() == 0 && getAccessor() != null) {
185             Class returnType = getAccessor().getReturnType();
186             log.finer("Using accessor return type as state variable type: " + returnType);
187 
188             if (ModelUtil.isStringConvertibleType(getStringConvertibleTypes(), returnType)) {
189                 // Enums and toString() convertible types are always state variables with type STRING
190                 log.finer("Return type is string-convertible, using string datatype");
191                 return Datatype.Default.STRING.getBuiltinType().getDatatype();
192             } else {
193                 Datatype.Default defaultDatatype = Datatype.Default.getByJavaType(returnType);
194                 if (defaultDatatype != null) {
195                     log.finer("Return type has default UPnP datatype: " + defaultDatatype);
196                     return defaultDatatype.getBuiltinType().getDatatype();
197                 }
198             }
199         }
200 
201         // We can also guess that if the allowed values are set then it's a string
202         if ((declaredDatatype == null || declaredDatatype.length() == 0) &&
203                 (getAnnotation().allowedValues().length > 0 || getAnnotation().allowedValuesEnum() != void.class)) {
204             log.finer("State variable has restricted allowed values, hence using 'string' datatype");
205             declaredDatatype = "string";
206         }
207 
208         // If we still don't have it, there is nothing more we can do
209         if (declaredDatatype == null || declaredDatatype.length() == 0) {
210             throw new LocalServiceBindingException("Could not detect datatype of state variable: " + getName());
211         }
212 
213         log.finer("Trying to find built-in UPnP datatype for detected name: " + declaredDatatype);
214 
215         // Now try to find the actual UPnP datatype by mapping the Default to Builtin
216         Datatype.Builtin builtin = Datatype.Builtin.getByDescriptorName(declaredDatatype);
217         if (builtin != null) {
218             log.finer("Found built-in UPnP datatype: " + builtin);
219             return builtin.getDatatype();
220         } else {
221             // TODO
222             throw new LocalServiceBindingException("No built-in UPnP datatype found, using CustomDataType (TODO: NOT IMPLEMENTED)");
223         }
224     }
225 
226     protected String createDefaultValue(Datatype datatype) throws LocalServiceBindingException {
227 
228         // Next, the default value of the state variable, first the declared one
229         if (getAnnotation().defaultValue().length() != 0) {
230             // The declared default value needs to match the datatype
231             try {
232                 datatype.valueOf(getAnnotation().defaultValue());
233                 log.finer("Found state variable default value: " + getAnnotation().defaultValue());
234                 return getAnnotation().defaultValue();
235             } catch (Exception ex) {
236                 throw new LocalServiceBindingException(
237                         "Default value doesn't match datatype of state variable '" + getName() + "': " + ex.getMessage()
238                 );
239             }
240         }
241 
242         return null;
243     }
244 
245     protected String[] getAllowedValues(Class enumType) throws LocalServiceBindingException {
246 
247         if (!enumType.isEnum()) {
248             throw new LocalServiceBindingException("Allowed values type is not an Enum: " + enumType);
249         }
250 
251         log.finer("Restricting allowed values of state variable to Enum: " + getName());
252         String[] allowedValueStrings = new String[enumType.getEnumConstants().length];
253         for (int i = 0; i < enumType.getEnumConstants().length; i++) {
254             Object o = enumType.getEnumConstants()[i];
255             if (o.toString().length() > 32) {
256                 throw new LocalServiceBindingException(
257                         "Allowed value string (that is, Enum constant name) is longer than 32 characters: " + o.toString()
258                 );
259             }
260             log.finer("Adding allowed value (converted to string): " + o.toString());
261             allowedValueStrings[i] = o.toString();
262         }
263 
264         return allowedValueStrings;
265     }
266 
267     protected StateVariableAllowedValueRange getAllowedValueRange(long min,
268                                                                   long max,
269                                                                   long step) throws LocalServiceBindingException {
270         if (max < min) {
271             throw new LocalServiceBindingException(
272                     "Allowed value range maximum is smaller than minimum: " + getName()
273             );
274         }
275 
276         return new StateVariableAllowedValueRange(min, max, step);
277     }
278 
279     protected String[] getAllowedValuesFromProvider() throws LocalServiceBindingException {
280         Class provider = getAnnotation().allowedValueProvider();
281         if (!AllowedValueProvider.class.isAssignableFrom(provider))
282             throw new LocalServiceBindingException(
283                 "Allowed value provider is not of type " + AllowedValueProvider.class + ": " + getName()
284             );
285         try {
286             return ((Class<? extends AllowedValueProvider>) provider).newInstance().getValues();
287         } catch (Exception ex) {
288             throw new LocalServiceBindingException(
289                 "Allowed value provider can't be instantiated: " + getName(), ex
290             );
291         }
292     }
293 
294     protected StateVariableAllowedValueRange getAllowedRangeFromProvider() throws  LocalServiceBindingException {
295         Class provider = getAnnotation().allowedValueRangeProvider();
296         if (!AllowedValueRangeProvider.class.isAssignableFrom(provider))
297             throw new LocalServiceBindingException(
298                 "Allowed value range provider is not of type " + AllowedValueRangeProvider.class + ": " + getName()
299             );
300         try {
301             AllowedValueRangeProvider providerInstance =
302                 ((Class<? extends AllowedValueRangeProvider>) provider).newInstance();
303             return getAllowedValueRange(
304                 providerInstance.getMinimum(),
305                 providerInstance.getMaximum(),
306                 providerInstance.getStep()
307             );
308         } catch (Exception ex) {
309             throw new LocalServiceBindingException(
310                 "Allowed value range provider can't be instantiated: " + getName(), ex
311             );
312         }
313     }
314 
315 
316 }