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.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   * @author Christian Bauer
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         // TODO: Invent an annotation for this configuration
104         return new MethodActionExecutor(outputArguments, getMethod());
105     }
106 
107     protected List<ActionArgument> createInputArguments() throws LocalServiceBindingException {
108 
109         List<ActionArgument> list = new ArrayList<>();
110 
111         // Input arguments are always method parameters
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         // A method can't have any parameters that are not annotated with @UpnpInputArgument - we wouldn't know what
151         // value to pass when we invoke it later on... unless the last parameter is of type RemoteClientInfo
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<>(); // !!! Insertion order!
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             // Might-just-work attempt, try the name of the getter
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                 // Use the same class as the action method
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             // Use the returned class
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             // Try with A_ARG_TYPE prefix
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             // TODO: Well, this is often a nice shortcut but sometimes might have false positives
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         // Validate datatype as good as we can
301         // (for enums and other convertible types, the state variable type should be STRING)
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             // TODO: Consider custom types?!
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 }