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}