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.LocalServiceBinder;
19  import org.fourthline.cling.binding.LocalServiceBindingException;
20  import org.fourthline.cling.model.ValidationError;
21  import org.fourthline.cling.model.ValidationException;
22  import org.fourthline.cling.model.action.ActionExecutor;
23  import org.fourthline.cling.model.action.QueryStateVariableExecutor;
24  import org.fourthline.cling.model.meta.Action;
25  import org.fourthline.cling.model.meta.LocalService;
26  import org.fourthline.cling.model.meta.QueryStateVariableAction;
27  import org.fourthline.cling.model.meta.StateVariable;
28  import org.fourthline.cling.model.state.FieldStateVariableAccessor;
29  import org.fourthline.cling.model.state.GetterStateVariableAccessor;
30  import org.fourthline.cling.model.state.StateVariableAccessor;
31  import org.fourthline.cling.model.types.ServiceId;
32  import org.fourthline.cling.model.types.ServiceType;
33  import org.fourthline.cling.model.types.UDAServiceId;
34  import org.fourthline.cling.model.types.UDAServiceType;
35  import org.fourthline.cling.model.types.csv.CSV;
36  import org.seamless.util.Reflections;
37  
38  import java.lang.reflect.Field;
39  import java.lang.reflect.Method;
40  import java.lang.reflect.Modifier;
41  import java.net.URI;
42  import java.net.URL;
43  import java.util.Arrays;
44  import java.util.HashMap;
45  import java.util.HashSet;
46  import java.util.Map;
47  import java.util.Set;
48  import java.util.Locale;
49  import java.util.logging.Logger;
50  
51  /**
52   * Reads {@link org.fourthline.cling.model.meta.LocalService} metadata from annotations.
53   *
54   * @author Christian Bauer
55   */
56  public class AnnotationLocalServiceBinder implements LocalServiceBinder {
57  
58      private static Logger log = Logger.getLogger(AnnotationLocalServiceBinder.class.getName());
59  
60      public LocalService read(Class<?> clazz) throws LocalServiceBindingException {
61          log.fine("Reading and binding annotations of service implementation class: " + clazz);
62  
63          // Read the service ID and service type from the annotation
64          if (clazz.isAnnotationPresent(UpnpService.class)) {
65  
66              UpnpService annotation = clazz.getAnnotation(UpnpService.class);
67              UpnpServiceId idAnnotation = annotation.serviceId();
68              UpnpServiceType typeAnnotation = annotation.serviceType();
69  
70              ServiceId serviceId = idAnnotation.namespace().equals(UDAServiceId.DEFAULT_NAMESPACE)
71                      ? new UDAServiceId(idAnnotation.value())
72                      : new ServiceId(idAnnotation.namespace(), idAnnotation.value());
73  
74              ServiceType serviceType = typeAnnotation.namespace().equals(UDAServiceType.DEFAULT_NAMESPACE)
75                      ? new UDAServiceType(typeAnnotation.value(), typeAnnotation.version())
76                      : new ServiceType(typeAnnotation.namespace(), typeAnnotation.value(), typeAnnotation.version());
77  
78              boolean supportsQueryStateVariables = annotation.supportsQueryStateVariables();
79  
80              Set<Class> stringConvertibleTypes = readStringConvertibleTypes(annotation.stringConvertibleTypes());
81  
82              return read(clazz, serviceId, serviceType, supportsQueryStateVariables, stringConvertibleTypes);
83          } else {
84              throw new LocalServiceBindingException("Given class is not an @UpnpService");
85          }
86      }
87  
88      public LocalService read(Class<?> clazz, ServiceId id, ServiceType type,
89                               boolean supportsQueryStateVariables, Class[] stringConvertibleTypes) throws LocalServiceBindingException {
90          return read(clazz, id, type, supportsQueryStateVariables, new HashSet<>(Arrays.asList(stringConvertibleTypes)));
91      }
92  
93      public LocalService read(Class<?> clazz, ServiceId id, ServiceType type,
94                                     boolean supportsQueryStateVariables, Set<Class> stringConvertibleTypes)
95              throws LocalServiceBindingException {
96  
97          Map<StateVariable, StateVariableAccessor> stateVariables = readStateVariables(clazz, stringConvertibleTypes);
98          Map<Action, ActionExecutor> actions = readActions(clazz, stateVariables, stringConvertibleTypes);
99  
100         // Special treatment of the state variable querying action
101         if (supportsQueryStateVariables) {
102             actions.put(new QueryStateVariableAction(), new QueryStateVariableExecutor());
103         }
104 
105         try {
106             return new LocalService(type, id, actions, stateVariables, stringConvertibleTypes, supportsQueryStateVariables);
107 
108         } catch (ValidationException ex) {
109             log.severe("Could not validate device model: " + ex.toString());
110             for (ValidationError validationError : ex.getErrors()) {
111                 log.severe(validationError.toString());
112             }
113             throw new LocalServiceBindingException("Validation of model failed, check the log");
114         }
115     }
116 
117     protected Set<Class> readStringConvertibleTypes(Class[] declaredTypes) throws LocalServiceBindingException {
118 
119         for (Class stringConvertibleType : declaredTypes) {
120             if (!Modifier.isPublic(stringConvertibleType.getModifiers())) {
121                 throw new LocalServiceBindingException(
122                         "Declared string-convertible type must be public: " + stringConvertibleType
123                 );
124             }
125             try {
126                 stringConvertibleType.getConstructor(String.class);
127             } catch (NoSuchMethodException ex) {
128                 throw new LocalServiceBindingException(
129                         "Declared string-convertible type needs a public single-argument String constructor: " + stringConvertibleType
130                 );
131             }
132         }
133         Set<Class> stringConvertibleTypes = new HashSet(Arrays.asList(declaredTypes));
134 
135         // Some defaults
136         stringConvertibleTypes.add(URI.class);
137         stringConvertibleTypes.add(URL.class);
138         stringConvertibleTypes.add(CSV.class);
139 
140         return stringConvertibleTypes;
141     }
142 
143     protected Map<StateVariable, StateVariableAccessor> readStateVariables(Class<?> clazz, Set<Class> stringConvertibleTypes)
144             throws LocalServiceBindingException {
145 
146         Map<StateVariable, StateVariableAccessor> map = new HashMap<>();
147 
148         // State variables declared on the class
149         if (clazz.isAnnotationPresent(UpnpStateVariables.class)) {
150             UpnpStateVariables variables = clazz.getAnnotation(UpnpStateVariables.class);
151             for (UpnpStateVariable v : variables.value()) {
152 
153                 if (v.name().length() == 0)
154                     throw new LocalServiceBindingException("Class-level @UpnpStateVariable name attribute value required");
155 
156                 String javaPropertyName = toJavaStateVariableName(v.name());
157 
158                 Method getter = Reflections.getGetterMethod(clazz, javaPropertyName);
159                 Field field = Reflections.getField(clazz, javaPropertyName);
160 
161                 StateVariableAccessor accessor = null;
162                 if (getter != null && field != null) {
163                     accessor = variables.preferFields() ?
164                             new FieldStateVariableAccessor(field)
165                             : new GetterStateVariableAccessor(getter);
166                 } else if (field != null) {
167                     accessor = new FieldStateVariableAccessor(field);
168                 } else if (getter != null) {
169                     accessor = new GetterStateVariableAccessor(getter);
170                 } else {
171                     log.finer("No field or getter found for state variable, skipping accessor: " + v.name());
172                 }
173 
174                 StateVariable stateVar =
175                         new AnnotationStateVariableBinder(v, v.name(), accessor, stringConvertibleTypes)
176                                 .createStateVariable();
177 
178                 map.put(stateVar, accessor);
179             }
180         }
181 
182         // State variables declared on fields
183         for (Field field : Reflections.getFields(clazz, UpnpStateVariable.class)) {
184 
185             UpnpStateVariable svAnnotation = field.getAnnotation(UpnpStateVariable.class);
186 
187             StateVariableAccessor accessor = new FieldStateVariableAccessor(field);
188 
189             StateVariable stateVar = new AnnotationStateVariableBinder(
190                     svAnnotation,
191                     svAnnotation.name().length() == 0
192                             ? toUpnpStateVariableName(field.getName())
193                             : svAnnotation.name(),
194                     accessor,
195                     stringConvertibleTypes
196             ).createStateVariable();
197 
198             map.put(stateVar, accessor);
199         }
200 
201         // State variables declared on getters
202         for (Method getter : Reflections.getMethods(clazz, UpnpStateVariable.class)) {
203 
204             String propertyName = Reflections.getMethodPropertyName(getter.getName());
205             if (propertyName == null) {
206                 throw new LocalServiceBindingException(
207                         "Annotated method is not a getter method (: " + getter
208                 );
209             }
210 
211             if (getter.getParameterTypes().length > 0)
212                 throw new LocalServiceBindingException(
213                         "Getter method defined as @UpnpStateVariable can not have parameters: " + getter
214                 );
215 
216             UpnpStateVariable svAnnotation = getter.getAnnotation(UpnpStateVariable.class);
217 
218             StateVariableAccessor accessor = new GetterStateVariableAccessor(getter);
219 
220             StateVariable stateVar = new AnnotationStateVariableBinder(
221                     svAnnotation,
222                     svAnnotation.name().length() == 0
223                             ?
224                             toUpnpStateVariableName(propertyName)
225                             : svAnnotation.name(),
226                     accessor,
227                     stringConvertibleTypes
228             ).createStateVariable();
229 
230             map.put(stateVar, accessor);
231         }
232 
233         return map;
234     }
235 
236     protected Map<Action, ActionExecutor> readActions(Class<?> clazz,
237                                                       Map<StateVariable, StateVariableAccessor> stateVariables,
238                                                       Set<Class> stringConvertibleTypes)
239             throws LocalServiceBindingException {
240 
241         Map<Action, ActionExecutor> map = new HashMap<>();
242 
243         for (Method method : Reflections.getMethods(clazz, UpnpAction.class)) {
244             AnnotationActionBinder actionBinder =
245                     new AnnotationActionBinder(method, stateVariables, stringConvertibleTypes);
246             Action action = actionBinder.appendAction(map);
247             if(isActionExcluded(action)) {
248             	map.remove(action);
249             }
250         }
251 
252         return map;
253     }
254 
255     /**
256      * Override this method to exclude action/methods after they have been discovered.
257      */
258     protected  boolean isActionExcluded(Action action) {
259     	return false;
260     }
261     
262     // TODO: I don't like the exceptions much, user has no idea what to do
263 
264     static String toUpnpStateVariableName(String javaName) {
265         if (javaName.length() < 1) {
266             throw new IllegalArgumentException("Variable name must be at least 1 character long");
267         }
268         return javaName.substring(0, 1).toUpperCase(Locale.ROOT) + javaName.substring(1);
269     }
270 
271     static String toJavaStateVariableName(String upnpName) {
272         if (upnpName.length() < 1) {
273             throw new IllegalArgumentException("Variable name must be at least 1 character long");
274         }
275         return upnpName.substring(0, 1).toLowerCase(Locale.ROOT) + upnpName.substring(1);
276     }
277 
278 
279     static String toUpnpActionName(String javaName) {
280         if (javaName.length() < 1) {
281             throw new IllegalArgumentException("Action name must be at least 1 character long");
282         }
283         return javaName.substring(0, 1).toUpperCase(Locale.ROOT) + javaName.substring(1);
284     }
285 
286     static String toJavaActionName(String upnpName) {
287         if (upnpName.length() < 1) {
288             throw new IllegalArgumentException("Variable name must be at least 1 character long");
289         }
290         return upnpName.substring(0, 1).toLowerCase(Locale.ROOT) + upnpName.substring(1);
291     }
292 
293 }