001/*
002 *  Copyright 2013 Anyware Services
003 *
004 *  Licensed under the Apache License, Version 2.0 (the "License");
005 *  you may not use this file except in compliance with the License.
006 *  You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 *  Unless required by applicable law or agreed to in writing, software
011 *  distributed under the License is distributed on an "AS IS" BASIS,
012 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 *  See the License for the specific language governing permissions and
014 *  limitations under the License.
015 */
016package org.ametys.core.ui;
017
018import java.lang.reflect.Method;
019import java.util.List;
020import java.util.Map;
021
022import org.apache.avalon.framework.parameters.Parameters;
023import org.apache.avalon.framework.service.ServiceException;
024import org.apache.avalon.framework.thread.ThreadSafe;
025import org.apache.cocoon.acting.ServiceableAction;
026import org.apache.cocoon.environment.ObjectModelHelper;
027import org.apache.cocoon.environment.Redirector;
028import org.apache.cocoon.environment.Request;
029import org.apache.cocoon.environment.SourceResolver;
030import org.apache.commons.lang3.ClassUtils;
031import org.apache.commons.lang3.StringUtils;
032import org.apache.commons.lang3.reflect.MethodUtils;
033
034import org.ametys.core.cocoon.JSonReader;
035import org.ametys.core.right.RightAssignmentContext;
036import org.ametys.core.right.RightAssignmentContextExtensionPoint;
037import org.ametys.core.right.RightManager;
038import org.ametys.core.right.RightManager.RightResult;
039import org.ametys.core.user.CurrentUserProvider;
040import org.ametys.core.user.UserIdentity;
041import org.ametys.runtime.authentication.AccessDeniedException;
042import org.ametys.runtime.plugin.ExtensionPoint;
043
044/**
045 * Action executing remote method calls coming from client-side elements.<br>
046 * Called methods should be annotated with {@link Callable}.<br>
047 */
048public class ExecuteClientCallsAction extends ServiceableAction implements ThreadSafe
049{
050    private RightManager _rightManager;
051    private CurrentUserProvider _currentUserProvider;
052    private RightAssignmentContextExtensionPoint _rightCtxEP;
053    
054    private RightManager _getRightManager()
055    {
056        if (_rightManager == null)
057        {
058            try
059            {
060                _rightManager = (RightManager) manager.lookup(RightManager.ROLE);
061            }
062            catch (ServiceException e)
063            {
064                throw new RuntimeException(e);
065            }
066        }
067        
068        return _rightManager;
069    }
070    
071    private CurrentUserProvider _getCurrentUserProvider()
072    {
073        if (_currentUserProvider == null)
074        {
075            try
076            {
077                _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE);
078            }
079            catch (ServiceException e)
080            {
081                throw new RuntimeException(e);
082            }
083        }
084        return _currentUserProvider;
085    }
086    
087    
088    private RightAssignmentContextExtensionPoint _getRightContextEP()
089    {
090        if (_rightCtxEP == null)
091        {
092            try
093            {
094                _rightCtxEP = (RightAssignmentContextExtensionPoint) manager.lookup(RightAssignmentContextExtensionPoint.ROLE);
095            }
096            catch (ServiceException e)
097            {
098                throw new RuntimeException(e);
099            }
100        }
101        return _rightCtxEP;
102    }
103    
104    @SuppressWarnings("unchecked")
105    @Override
106    public Map act(Redirector redirector, SourceResolver resolver, Map objectModel, String source, Parameters parameters) throws Exception
107    {
108        Map<String, Object> jsParameters = (Map<String, Object>) objectModel.get(ObjectModelHelper.PARENT_CONTEXT);
109
110        // Find the corresponding object, either a component or an extension
111        String role = (String) jsParameters.get("role");
112        
113        if (role == null)
114        {
115            throw new IllegalArgumentException("Component role should be present.");
116        }
117        
118        Object object;
119        
120        if (!manager.hasService(role))
121        {
122            throw new IllegalArgumentException("The role '" + role + "' does not correspond to a valid component.");
123        }
124        
125        Object component = manager.lookup(role);
126        
127        if (component instanceof ExtensionPoint)
128        {
129            ExtensionPoint extPoint = (ExtensionPoint) component;
130            
131            String id = (String) jsParameters.get("id");
132            
133            if (id == null)
134            {
135                object = component;
136            }
137            else
138            {
139                object = extPoint.getExtension(id);
140                
141                if (object == null)
142                {
143                    throw new IllegalArgumentException("The id '" + id + "' does not correspond to a valid extension for point " + role);
144                }
145            }
146        }
147        else
148        {
149            object = component;
150        }
151        
152        // Find the corresponding method
153        String methodName = (String) jsParameters.get("methodName");
154        List<Object> params = (List<Object>) jsParameters.get("parameters");
155        
156        if (methodName == null)
157        {
158            throw new IllegalArgumentException("No method name present, cannot execute server side code.");
159        }
160        
161        Class[] paramClass;
162        Object[] paramValues;
163        if (params == null)
164        {
165            paramClass = new Class[0];
166            paramValues = new Object[0];
167        }
168        else
169        {
170            paramValues = params.toArray();
171            paramClass = ClassUtils.toClass(paramValues);
172        }
173        
174        Class<? extends Object> clazz = object.getClass();
175        Method method = MethodUtils.getMatchingAccessibleMethod(clazz, methodName, paramClass);
176        
177        if (method == null)
178        {
179            throw new IllegalArgumentException("No method with signature " + methodName + "(" + StringUtils.join(paramClass, ", ").replaceAll("class ", "") + ") present in class " + clazz.getName() + ".");
180        }
181        
182        Object result = _executeMethod(method, object, paramValues);
183        
184        Request request = ObjectModelHelper.getRequest(objectModel);
185        request.setAttribute(JSonReader.OBJECT_TO_READ, result);
186        
187        return EMPTY_MAP;
188    }
189    
190    /**
191     * Execute the method set in the client call
192     * @param method The method
193     * @param object The object which has the method
194     * @param paramValues The method parameters
195     * @return The result
196     * @throws Exception If an error occurred
197     */
198    protected Object _executeMethod(Method method, Object object, Object[] paramValues) throws Exception
199    {
200        Object result = null;
201        if (method.isAnnotationPresent(Callable.class))
202        {
203            _checkAccess(method, paramValues);
204            result = method.invoke(object, paramValues);
205        }
206        else
207        {
208            throw new IllegalArgumentException("Trying to call a non-callable method: " + method.toGenericString() + ".");
209        }
210        
211        return result;
212    }
213    
214    private void _checkAccess(Method method, Object[] paramValues)
215    {
216        Callable callable = method.getAnnotation(Callable.class);
217        if (StringUtils.isNotEmpty(callable.right()))
218        {
219            UserIdentity currentUser = _getCurrentUserProvider().getUser();
220            Object context = _getRightContext(method, callable, paramValues);
221            
222            if (_getRightManager().hasRight(currentUser, callable.right(), context) != RightResult.RIGHT_ALLOW)
223            {
224                throw new AccessDeniedException("The user " + currentUser + " tried to access the callable method [" + method.toGenericString() + "] without sufficient rights");
225            }
226        }
227    }
228    
229    private Object _getRightContext(Method method, Callable callable, Object[] paramValues)
230    {
231        if (StringUtils.isNotEmpty(callable.rightContext()))
232        {
233            int index = callable.paramIndex();
234            if (index < 0 || index > paramValues.length - 1)
235            {
236                throw new IllegalArgumentException("Callable method [" + method.toGenericString() + "] refers to a invalid 'paramIndex' " + index + ".");
237            }
238            
239            Object jsContext = paramValues[index];
240            String rightCtxId = callable.rightContext();
241            
242            RightAssignmentContext rightCtx = _getRightContextEP().getExtension(rightCtxId);
243            if (rightCtx == null)
244            {
245                throw new IllegalArgumentException("Callable method [" + method.toGenericString() + "] refers to a unknown 'rightContext' of id " + rightCtxId + ".");
246            }
247            
248            Object context = rightCtx.convertJSContext(jsContext);
249            if (context == null)
250            {
251                throw new IllegalArgumentException("Right object context not found for value " + jsContext +  ". Unable to check right for callable method: " + method.toGenericString() + ".");
252            }
253            return context;
254        }
255        else
256        {
257            return callable.context();
258        }
259    }
260}