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.model.action;
17  
18  import org.fourthline.cling.model.meta.ActionArgument;
19  import org.fourthline.cling.model.meta.LocalService;
20  import org.fourthline.cling.model.profile.RemoteClientInfo;
21  import org.fourthline.cling.model.state.StateVariableAccessor;
22  import org.fourthline.cling.model.types.ErrorCode;
23  import org.seamless.util.Reflections;
24  
25  import java.lang.reflect.Constructor;
26  import java.lang.reflect.Method;
27  import java.util.ArrayList;
28  import java.util.List;
29  import java.util.Map;
30  import java.util.logging.Logger;
31  
32  /**
33   * Invokes methods on a service implementation instance with reflection.
34   *
35   * <p>
36   * If the method has an additional last parameter of type
37   * {@link org.fourthline.cling.model.profile.RemoteClientInfo}, the details
38   * of the control point client will be provided to the action method. You can use this
39   * to get the client's address and request headers, and to provide extra response headers.
40   * </p>
41   *
42   * @author Christian Bauer
43   */
44  public class MethodActionExecutor extends AbstractActionExecutor {
45  
46      private static Logger log = Logger.getLogger(MethodActionExecutor.class.getName());
47  
48      protected Method method;
49  
50      public MethodActionExecutor(Method method) {
51          this.method = method;
52      }
53  
54      public MethodActionExecutor(Map<ActionArgument<LocalService>, StateVariableAccessor> outputArgumentAccessors, Method method) {
55          super(outputArgumentAccessors);
56          this.method = method;
57      }
58  
59      public Method getMethod() {
60          return method;
61      }
62  
63      @Override
64      protected void execute(ActionInvocation<LocalService> actionInvocation, Object serviceImpl) throws Exception {
65  
66          // Find the "real" parameters of the method we want to call, and create arguments
67          Object[] inputArgumentValues = createInputArgumentValues(actionInvocation, method);
68  
69          // Simple case: no output arguments
70          if (!actionInvocation.getAction().hasOutputArguments()) {
71              log.fine("Calling local service method with no output arguments: " + method);
72              Reflections.invoke(method, serviceImpl, inputArgumentValues);
73              return;
74          }
75  
76          boolean isVoid = method.getReturnType().equals(Void.TYPE);
77  
78          log.fine("Calling local service method with output arguments: " + method);
79          Object result;
80          boolean isArrayResultProcessed = true;
81          if (isVoid) {
82  
83              log.fine("Action method is void, calling declared accessors(s) on service instance to retrieve ouput argument(s)");
84              Reflections.invoke(method, serviceImpl, inputArgumentValues);
85              result = readOutputArgumentValues(actionInvocation.getAction(), serviceImpl);
86  
87          } else if (isUseOutputArgumentAccessors(actionInvocation)) {
88  
89              log.fine("Action method is not void, calling declared accessor(s) on returned instance to retrieve ouput argument(s)");
90              Object returnedInstance = Reflections.invoke(method, serviceImpl, inputArgumentValues);
91              result = readOutputArgumentValues(actionInvocation.getAction(), returnedInstance);
92  
93          } else {
94  
95              log.fine("Action method is not void, using returned value as (single) output argument");
96              result = Reflections.invoke(method, serviceImpl, inputArgumentValues);
97              isArrayResultProcessed = false; // We never want to process e.g. byte[] as individual variable values
98          }
99  
100         ActionArgument<LocalService>[] outputArgs = actionInvocation.getAction().getOutputArguments();
101 
102         if (isArrayResultProcessed && result instanceof Object[]) {
103             Object[] results = (Object[]) result;
104             log.fine("Accessors returned Object[], setting output argument values: " + results.length);
105             for (int i = 0; i < outputArgs.length; i++) {
106                 setOutputArgumentValue(actionInvocation, outputArgs[i], results[i]);
107             }
108         } else if (outputArgs.length == 1) {
109             setOutputArgumentValue(actionInvocation, outputArgs[0], result);
110         } else {
111             throw new ActionException(
112                     ErrorCode.ACTION_FAILED,
113                     "Method return does not match required number of output arguments: " + outputArgs.length
114             );
115         }
116 
117     }
118 
119     protected boolean isUseOutputArgumentAccessors(ActionInvocation<LocalService> actionInvocation) {
120         for (ActionArgument argument : actionInvocation.getAction().getOutputArguments()) {
121             // If there is one output argument for which we have an accessor, all arguments need accessors
122             if (getOutputArgumentAccessors().get(argument) != null) {
123                 return true;
124             }
125         }
126         return false;
127     }
128 
129     protected Object[] createInputArgumentValues(ActionInvocation<LocalService> actionInvocation, Method method) throws ActionException {
130 
131         LocalService service = actionInvocation.getAction().getService();
132 
133         List values = new ArrayList<>();
134         int i = 0;
135         for (ActionArgument<LocalService> argument : actionInvocation.getAction().getInputArguments()) {
136 
137             Class methodParameterType = method.getParameterTypes()[i];
138 
139             ActionArgumentValue<LocalService> inputValue = actionInvocation.getInput(argument);
140 
141             // If it's a primitive argument, we need a value
142             if (methodParameterType.isPrimitive() && (inputValue == null || inputValue.toString().length() == 0))
143                 throw new ActionException(
144                         ErrorCode.ARGUMENT_VALUE_INVALID,
145                         "Primitive action method argument '" + argument.getName() + "' requires input value, can't be null or empty string"
146                 );
147 
148             // It's not primitive and we have no value, that's fine too
149             if (inputValue == null) {
150                 values.add(i++, null);
151                 continue;
152             }
153 
154             // If it's not null, maybe it was a string-convertible type, if so, try to instantiate it
155             String inputCallValueString = inputValue.toString();
156             // Empty string means null and we can't instantiate Enums!
157             if (inputCallValueString.length() > 0 && service.isStringConvertibleType(methodParameterType) && !methodParameterType.isEnum()) {
158                 try {
159                     Constructor<String> ctor = methodParameterType.getConstructor(String.class);
160                     log.finer("Creating new input argument value instance with String.class constructor of type: " + methodParameterType);
161                     Object o = ctor.newInstance(inputCallValueString);
162                     values.add(i++, o);
163                 } catch (Exception ex) {
164                     log.warning("Error preparing action method call: " + method);
165                     log.warning("Can't convert input argument string to desired type of '" + argument.getName() + "': " + ex);
166                     throw new ActionException(
167                             ErrorCode.ARGUMENT_VALUE_INVALID, "Can't convert input argument string to desired type of '" + argument.getName() + "': " + ex
168                     );
169                 }
170             } else {
171                 // Or if it wasn't, just use the value without any conversion
172                 values.add(i++, inputValue.getValue());
173             }
174         }
175 
176         if (method.getParameterTypes().length > 0
177             && RemoteClientInfo.class.isAssignableFrom(method.getParameterTypes()[method.getParameterTypes().length-1])) {
178             if (actionInvocation instanceof RemoteActionInvocation &&
179                 ((RemoteActionInvocation)actionInvocation).getRemoteClientInfo() != null) {
180                 log.finer("Providing remote client info as last action method input argument: " + method);
181                 values.add(i, ((RemoteActionInvocation)actionInvocation).getRemoteClientInfo());
182             } else {
183                 // Local call, no client info available
184                 values.add(i, null);
185             }
186         }
187 
188         return values.toArray(new Object[values.size()]);
189     }
190 
191 }