1
2
3
4
5
6
7
8
9
10
11
12
13
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
53
54
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
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
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
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
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
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
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
257
258 protected boolean isActionExcluded(Action action) {
259 return false;
260 }
261
262
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 }