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.LocalServiceBindingException;
19 import org.fourthline.cling.model.Constants;
20 import org.fourthline.cling.model.ModelUtil;
21 import org.fourthline.cling.model.action.ActionExecutor;
22 import org.fourthline.cling.model.action.MethodActionExecutor;
23 import org.fourthline.cling.model.meta.Action;
24 import org.fourthline.cling.model.meta.ActionArgument;
25 import org.fourthline.cling.model.meta.LocalService;
26 import org.fourthline.cling.model.meta.StateVariable;
27 import org.fourthline.cling.model.profile.RemoteClientInfo;
28 import org.fourthline.cling.model.state.GetterStateVariableAccessor;
29 import org.fourthline.cling.model.state.StateVariableAccessor;
30 import org.fourthline.cling.model.types.Datatype;
31 import org.seamless.util.Reflections;
32
33 import java.lang.annotation.Annotation;
34 import java.lang.reflect.Method;
35 import java.util.ArrayList;
36 import java.util.LinkedHashMap;
37 import java.util.List;
38 import java.util.Map;
39 import java.util.Set;
40 import java.util.logging.Logger;
41
42
43
44
45 public class AnnotationActionBinder {
46
47 private static Logger log = Logger.getLogger(AnnotationLocalServiceBinder.class.getName());
48
49 protected UpnpAction annotation;
50 protected Method method;
51 protected Map<StateVariable, StateVariableAccessor> stateVariables;
52 protected Set<Class> stringConvertibleTypes;
53
54 public AnnotationActionBinder(Method method, Map<StateVariable, StateVariableAccessor> stateVariables, Set<Class> stringConvertibleTypes) {
55 this.annotation = method.getAnnotation(UpnpAction.class);
56 this.stateVariables = stateVariables;
57 this.method = method;
58 this.stringConvertibleTypes = stringConvertibleTypes;
59 }
60
61 public UpnpAction getAnnotation() {
62 return annotation;
63 }
64
65 public Map<StateVariable, StateVariableAccessor> getStateVariables() {
66 return stateVariables;
67 }
68
69 public Method getMethod() {
70 return method;
71 }
72
73 public Set<Class> getStringConvertibleTypes() {
74 return stringConvertibleTypes;
75 }
76
77 public Action appendAction(Map<Action, ActionExecutor> actions) throws LocalServiceBindingException {
78
79 String name;
80 if (getAnnotation().name().length() != 0) {
81 name = getAnnotation().name();
82 } else {
83 name = AnnotationLocalServiceBinder.toUpnpActionName(getMethod().getName());
84 }
85
86 log.fine("Creating action and executor: " + name);
87
88 List<ActionArgument> inputArguments = createInputArguments();
89 Map<ActionArgument<LocalService>, StateVariableAccessor> outputArguments = createOutputArguments();
90
91 inputArguments.addAll(outputArguments.keySet());
92 ActionArgument<LocalService>[] actionArguments =
93 inputArguments.toArray(new ActionArgument[inputArguments.size()]);
94
95 Action action = new Action(name, actionArguments);
96 ActionExecutor executor = createExecutor(outputArguments);
97
98 actions.put(action, executor);
99 return action;
100 }
101
102 protected ActionExecutor createExecutor(Map<ActionArgument<LocalService>, StateVariableAccessor> outputArguments) {
103
104 return new MethodActionExecutor(outputArguments, getMethod());
105 }
106
107 protected List<ActionArgument> createInputArguments() throws LocalServiceBindingException {
108
109 List<ActionArgument> list = new ArrayList<>();
110
111
112 int annotatedParams = 0;
113 Annotation[][] params = getMethod().getParameterAnnotations();
114 for (int i = 0; i < params.length; i++) {
115 Annotation[] param = params[i];
116 for (Annotation paramAnnotation : param) {
117 if (paramAnnotation instanceof UpnpInputArgument) {
118 UpnpInputArgument inputArgumentAnnotation = (UpnpInputArgument) paramAnnotation;
119 annotatedParams++;
120
121 String argumentName =
122 inputArgumentAnnotation.name();
123
124 StateVariable stateVariable =
125 findRelatedStateVariable(
126 inputArgumentAnnotation.stateVariable(),
127 argumentName,
128 getMethod().getName()
129 );
130
131 if (stateVariable == null) {
132 throw new LocalServiceBindingException(
133 "Could not detected related state variable of argument: " + argumentName
134 );
135 }
136
137 validateType(stateVariable, getMethod().getParameterTypes()[i]);
138
139 ActionArgument inputArgument = new ActionArgument(
140 argumentName,
141 inputArgumentAnnotation.aliases(),
142 stateVariable.getName(),
143 ActionArgument.Direction.IN
144 );
145
146 list.add(inputArgument);
147 }
148 }
149 }
150
151
152 if (annotatedParams < getMethod().getParameterTypes().length
153 && !RemoteClientInfo.class.isAssignableFrom(method.getParameterTypes()[method.getParameterTypes().length-1])) {
154 throw new LocalServiceBindingException("Method has parameters that are not input arguments: " + getMethod().getName());
155 }
156
157 return list;
158 }
159
160 protected Map<ActionArgument<LocalService>, StateVariableAccessor> createOutputArguments() throws LocalServiceBindingException {
161
162 Map<ActionArgument<LocalService>, StateVariableAccessor> map = new LinkedHashMap<>();
163
164 UpnpAction actionAnnotation = getMethod().getAnnotation(UpnpAction.class);
165 if (actionAnnotation.out().length == 0) return map;
166
167 boolean hasMultipleOutputArguments = actionAnnotation.out().length > 1;
168
169 for (UpnpOutputArgument outputArgumentAnnotation : actionAnnotation.out()) {
170
171 String argumentName = outputArgumentAnnotation.name();
172
173 StateVariable stateVariable = findRelatedStateVariable(
174 outputArgumentAnnotation.stateVariable(),
175 argumentName,
176 getMethod().getName()
177 );
178
179
180 if (stateVariable == null && outputArgumentAnnotation.getterName().length() > 0) {
181 stateVariable = findRelatedStateVariable(null, null, outputArgumentAnnotation.getterName());
182 }
183
184 if (stateVariable == null) {
185 throw new LocalServiceBindingException(
186 "Related state variable not found for output argument: " + argumentName
187 );
188 }
189
190 StateVariableAccessor accessor = findOutputArgumentAccessor(
191 stateVariable,
192 outputArgumentAnnotation.getterName(),
193 hasMultipleOutputArguments
194 );
195
196 log.finer("Found related state variable for output argument '" + argumentName + "': " + stateVariable);
197
198 ActionArgument outputArgument = new ActionArgument(
199 argumentName,
200 stateVariable.getName(),
201 ActionArgument.Direction.OUT,
202 !hasMultipleOutputArguments
203 );
204
205 map.put(outputArgument, accessor);
206 }
207
208 return map;
209 }
210
211 protected StateVariableAccessor findOutputArgumentAccessor(StateVariable stateVariable, String getterName, boolean multipleArguments)
212 throws LocalServiceBindingException {
213
214 boolean isVoid = getMethod().getReturnType().equals(Void.TYPE);
215
216 if (isVoid) {
217
218 if (getterName != null && getterName.length() > 0) {
219 log.finer("Action method is void, will use getter method named: " + getterName);
220
221
222 Method getter = Reflections.getMethod(getMethod().getDeclaringClass(), getterName);
223 if (getter == null)
224 throw new LocalServiceBindingException(
225 "Declared getter method '" + getterName + "' not found on: " + getMethod().getDeclaringClass()
226 );
227
228 validateType(stateVariable, getter.getReturnType());
229
230 return new GetterStateVariableAccessor(getter);
231
232 } else {
233 log.finer("Action method is void, trying to find existing accessor of related: " + stateVariable);
234 return getStateVariables().get(stateVariable);
235 }
236
237
238 } else if (getterName != null && getterName.length() > 0) {
239 log.finer("Action method is not void, will use getter method on returned instance: " + getterName);
240
241
242 Method getter = Reflections.getMethod(getMethod().getReturnType(), getterName);
243 if (getter == null)
244 throw new LocalServiceBindingException(
245 "Declared getter method '" + getterName + "' not found on return type: " + getMethod().getReturnType()
246 );
247
248 validateType(stateVariable, getter.getReturnType());
249
250 return new GetterStateVariableAccessor(getter);
251
252 } else if (!multipleArguments) {
253 log.finer("Action method is not void, will use the returned instance: " + getMethod().getReturnType());
254 validateType(stateVariable, getMethod().getReturnType());
255 }
256
257 return null;
258 }
259
260 protected StateVariable findRelatedStateVariable(String declaredName, String argumentName, String methodName)
261 throws LocalServiceBindingException {
262
263 StateVariable relatedStateVariable = null;
264
265 if (declaredName != null && declaredName.length() > 0) {
266 relatedStateVariable = getStateVariable(declaredName);
267 }
268
269 if (relatedStateVariable == null && argumentName != null && argumentName.length() > 0) {
270 String actualName = AnnotationLocalServiceBinder.toUpnpStateVariableName(argumentName);
271 log.finer("Finding related state variable with argument name (converted to UPnP name): " + actualName);
272 relatedStateVariable = getStateVariable(argumentName);
273 }
274
275 if (relatedStateVariable == null && argumentName != null && argumentName.length() > 0) {
276
277 String actualName = AnnotationLocalServiceBinder.toUpnpStateVariableName(argumentName);
278 actualName = Constants.ARG_TYPE_PREFIX + actualName;
279 log.finer("Finding related state variable with prefixed argument name (converted to UPnP name): " + actualName);
280 relatedStateVariable = getStateVariable(actualName);
281 }
282
283 if (relatedStateVariable == null && methodName != null && methodName.length() > 0) {
284
285 String methodPropertyName = Reflections.getMethodPropertyName(methodName);
286 if (methodPropertyName != null) {
287 log.finer("Finding related state variable with method property name: " + methodPropertyName);
288 relatedStateVariable =
289 getStateVariable(
290 AnnotationLocalServiceBinder.toUpnpStateVariableName(methodPropertyName)
291 );
292 }
293 }
294
295 return relatedStateVariable;
296 }
297
298 protected void validateType(StateVariable stateVariable, Class type) throws LocalServiceBindingException {
299
300
301
302
303 Datatype.Default expectedDefaultMapping =
304 ModelUtil.isStringConvertibleType(getStringConvertibleTypes(), type)
305 ? Datatype.Default.STRING
306 : Datatype.Default.getByJavaType(type);
307
308 log.finer("Expecting '" + stateVariable + "' to match default mapping: " + expectedDefaultMapping);
309
310 if (expectedDefaultMapping != null &&
311 !stateVariable.getTypeDetails().getDatatype().isHandlingJavaType(expectedDefaultMapping.getJavaType())) {
312
313
314 throw new LocalServiceBindingException(
315 "State variable '" + stateVariable + "' datatype can't handle action " +
316 "argument's Java type (change one): " + expectedDefaultMapping.getJavaType()
317 );
318
319 } else if (expectedDefaultMapping == null && stateVariable.getTypeDetails().getDatatype().getBuiltin() != null) {
320 throw new LocalServiceBindingException(
321 "State variable '" + stateVariable + "' should be custom datatype " +
322 "(action argument type is unknown Java type): " + type.getSimpleName()
323 );
324 }
325
326 log.finer("State variable matches required argument datatype (or can't be validated because it is custom)");
327 }
328
329 protected StateVariable getStateVariable(String name) {
330 for (StateVariable stateVariable : getStateVariables().keySet()) {
331 if (stateVariable.getName().equals(name)) {
332 return stateVariable;
333 }
334 }
335 return null;
336 }
337
338 }