001/* 002 * Copyright 2002-2018 the original author or authors. 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 * https://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 */ 016 017package org.springframework.web.portlet.mvc.annotation; 018 019import java.lang.reflect.Method; 020import java.util.Arrays; 021import java.util.HashSet; 022import java.util.LinkedHashSet; 023import java.util.Set; 024import javax.portlet.ActionRequest; 025import javax.portlet.ClientDataRequest; 026import javax.portlet.Event; 027import javax.portlet.EventRequest; 028import javax.portlet.PortletException; 029import javax.portlet.PortletMode; 030import javax.portlet.PortletRequest; 031import javax.portlet.ResourceRequest; 032import javax.portlet.WindowState; 033 034import org.springframework.beans.BeansException; 035import org.springframework.context.ApplicationContext; 036import org.springframework.core.annotation.AnnotationUtils; 037import org.springframework.stereotype.Controller; 038import org.springframework.util.ReflectionUtils; 039import org.springframework.util.StringUtils; 040import org.springframework.web.bind.annotation.RequestMapping; 041import org.springframework.web.bind.annotation.RequestMethod; 042import org.springframework.web.portlet.bind.PortletRequestBindingException; 043import org.springframework.web.portlet.bind.annotation.ActionMapping; 044import org.springframework.web.portlet.bind.annotation.EventMapping; 045import org.springframework.web.portlet.bind.annotation.RenderMapping; 046import org.springframework.web.portlet.bind.annotation.ResourceMapping; 047import org.springframework.web.portlet.handler.AbstractMapBasedHandlerMapping; 048import org.springframework.web.portlet.handler.PortletRequestMethodNotSupportedException; 049 050/** 051 * Implementation of the {@link org.springframework.web.portlet.HandlerMapping} 052 * interface that maps handlers based on portlet modes expressed through the 053 * {@link RequestMapping} annotation at the type or method level. 054 * 055 * <p>Registered by default in {@link org.springframework.web.portlet.DispatcherPortlet}. 056 * <b>NOTE:</b> If you define custom HandlerMapping beans in your DispatcherPortlet context, 057 * you need to add a DefaultAnnotationHandlerMapping bean explicitly, since custom 058 * HandlerMapping beans replace the default mapping strategies. Defining a 059 * DefaultAnnotationHandlerMapping also allows for registering custom interceptors: 060 * 061 * <pre class="code"> 062 * <bean class="org.springframework.web.portlet.mvc.annotation.DefaultAnnotationHandlerMapping"> 063 * <property name="interceptors"> 064 * ... 065 * </property> 066 * </bean></pre> 067 * 068 * Annotated controllers are usually marked with the {@link Controller} stereotype 069 * at the type level. This is not strictly necessary when {@link RequestMapping} is 070 * applied at the type level (since such a handler usually implements the 071 * {@link org.springframework.web.portlet.mvc.Controller} interface). However, 072 * {@link Controller} is required for detecting {@link RequestMapping} annotations 073 * at the method level. 074 * 075 * <p><b>NOTE:</b> Method-level mappings are only allowed to narrow the mapping 076 * expressed at the class level (if any). A portlet mode in combination with specific 077 * parameter conditions needs to uniquely map onto one specific handler bean, 078 * not spread across multiple handler beans. It is strongly recommended to 079 * co-locate related handler methods into the same bean. 080 * 081 * <p>The {@link AnnotationMethodHandlerAdapter} is responsible for processing 082 * annotated handler methods, as mapped by this HandlerMapping. For 083 * {@link RequestMapping} at the type level, specific HandlerAdapters such as 084 * {@link org.springframework.web.portlet.mvc.SimpleControllerHandlerAdapter} apply. 085 * 086 * @author Juergen Hoeller 087 * @since 2.5 088 * @see RequestMapping 089 * @see AnnotationMethodHandlerAdapter 090 */ 091public class DefaultAnnotationHandlerMapping extends AbstractMapBasedHandlerMapping<PortletMode> { 092 093 /** 094 * Calls the {@code registerHandlers} method in addition 095 * to the superclass's initialization. 096 * @see #detectHandlers 097 */ 098 @Override 099 public void initApplicationContext() throws BeansException { 100 super.initApplicationContext(); 101 detectHandlers(); 102 } 103 104 /** 105 * Register all handlers specified in the Portlet mode map for the corresponding modes. 106 * @throws org.springframework.beans.BeansException if the handler couldn't be registered 107 */ 108 protected void detectHandlers() throws BeansException { 109 ApplicationContext context = getApplicationContext(); 110 String[] beanNames = context.getBeanNamesForType(Object.class); 111 for (String beanName : beanNames) { 112 Class<?> handlerType = context.getType(beanName); 113 RequestMapping mapping = context.findAnnotationOnBean(beanName, RequestMapping.class); 114 if (mapping != null) { 115 // @RequestMapping found at type level 116 String[] modeKeys = mapping.value(); 117 String[] params = mapping.params(); 118 boolean registerHandlerType = true; 119 if (modeKeys.length == 0 || params.length == 0) { 120 registerHandlerType = !detectHandlerMethods(handlerType, beanName, mapping); 121 } 122 if (registerHandlerType) { 123 AbstractParameterMappingPredicate predicate = new TypeLevelMappingPredicate( 124 params, mapping.headers(), mapping.method()); 125 for (String modeKey : modeKeys) { 126 registerHandler(new PortletMode(modeKey), beanName, predicate); 127 } 128 } 129 } 130 else if (AnnotationUtils.findAnnotation(handlerType, Controller.class) != null) { 131 detectHandlerMethods(handlerType, beanName, mapping); 132 } 133 } 134 } 135 136 /** 137 * Derive portlet mode mappings from the handler's method-level mappings. 138 * @param handlerType the handler type to introspect 139 * @param beanName the name of the bean introspected 140 * @param typeMapping the type level mapping (if any) 141 * @return {@code true} if at least 1 handler method has been registered; 142 * {@code false} otherwise 143 */ 144 protected boolean detectHandlerMethods(Class<?> handlerType, final String beanName, final RequestMapping typeMapping) { 145 final Set<Boolean> handlersRegistered = new HashSet<Boolean>(1); 146 Set<Class<?>> handlerTypes = new LinkedHashSet<Class<?>>(); 147 handlerTypes.add(handlerType); 148 handlerTypes.addAll(Arrays.asList(handlerType.getInterfaces())); 149 for (Class<?> currentHandlerType : handlerTypes) { 150 ReflectionUtils.doWithMethods(currentHandlerType, new ReflectionUtils.MethodCallback() { 151 @Override 152 public void doWith(Method method) { 153 PortletRequestMappingPredicate predicate = null; 154 String[] modeKeys = new String[0]; 155 String[] params = new String[0]; 156 if (typeMapping != null) { 157 params = PortletAnnotationMappingUtils.mergeStringArrays(typeMapping.params(), params); 158 } 159 ActionMapping actionMapping = AnnotationUtils.findAnnotation(method, ActionMapping.class); 160 RenderMapping renderMapping = AnnotationUtils.findAnnotation(method, RenderMapping.class); 161 ResourceMapping resourceMapping = AnnotationUtils.findAnnotation(method, ResourceMapping.class); 162 EventMapping eventMapping = AnnotationUtils.findAnnotation(method, EventMapping.class); 163 RequestMapping requestMapping = AnnotationUtils.findAnnotation(method, RequestMapping.class); 164 if (actionMapping != null) { 165 params = PortletAnnotationMappingUtils.mergeStringArrays(params, actionMapping.params()); 166 predicate = new ActionMappingPredicate(actionMapping.name(), params); 167 } 168 else if (renderMapping != null) { 169 params = PortletAnnotationMappingUtils.mergeStringArrays(params, renderMapping.params()); 170 predicate = new RenderMappingPredicate(renderMapping.windowState(), params); 171 } 172 else if (resourceMapping != null) { 173 predicate = new ResourceMappingPredicate(resourceMapping.value()); 174 } 175 else if (eventMapping != null) { 176 predicate = new EventMappingPredicate(eventMapping.value()); 177 } 178 if (requestMapping != null) { 179 modeKeys = requestMapping.value(); 180 if (typeMapping != null) { 181 if (!PortletAnnotationMappingUtils.validateModeMapping(modeKeys, typeMapping.value())) { 182 throw new IllegalStateException("Mode mappings conflict between method and type level: " + 183 Arrays.asList(modeKeys) + " versus " + Arrays.asList(typeMapping.value())); 184 } 185 } 186 params = PortletAnnotationMappingUtils.mergeStringArrays(params, requestMapping.params()); 187 if (predicate == null) { 188 predicate = new MethodLevelMappingPredicate(params); 189 } 190 } 191 if (predicate != null) { 192 if (modeKeys.length == 0) { 193 if (typeMapping != null) { 194 modeKeys = typeMapping.value(); 195 } 196 if (modeKeys.length == 0) { 197 throw new IllegalStateException( 198 "No portlet mode mappings specified - neither at type nor at method level"); 199 } 200 } 201 for (String modeKey : modeKeys) { 202 registerHandler(new PortletMode(modeKey), beanName, predicate); 203 handlersRegistered.add(Boolean.TRUE); 204 } 205 } 206 } 207 }, ReflectionUtils.USER_DECLARED_METHODS); 208 } 209 return !handlersRegistered.isEmpty(); 210 } 211 212 /** 213 * Uses the current PortletMode as lookup key. 214 */ 215 @Override 216 protected PortletMode getLookupKey(PortletRequest request) throws Exception { 217 return request.getPortletMode(); 218 } 219 220 221 private interface SpecialRequestTypePredicate { 222 } 223 224 225 private static abstract class AbstractParameterMappingPredicate implements PortletRequestMappingPredicate { 226 227 private final String[] params; 228 229 public AbstractParameterMappingPredicate(String[] params) { 230 this.params = params; 231 } 232 233 @Override 234 public boolean match(PortletRequest request) { 235 return PortletAnnotationMappingUtils.checkParameters(this.params, request); 236 } 237 238 protected int compareParams(AbstractParameterMappingPredicate other) { 239 return new Integer(other.params.length).compareTo(this.params.length); 240 } 241 242 protected int compareParams(Object other) { 243 if (other instanceof AbstractParameterMappingPredicate) { 244 return compareParams((AbstractParameterMappingPredicate) other); 245 } 246 return 0; 247 } 248 } 249 250 251 private static class TypeLevelMappingPredicate extends AbstractParameterMappingPredicate { 252 253 private final String[] headers; 254 255 private final Set<String> methods = new HashSet<String>(); 256 257 public TypeLevelMappingPredicate(String[] params, String[] headers, RequestMethod[] methods) { 258 super(params); 259 this.headers = headers; 260 if (methods != null) { 261 for (RequestMethod method : methods) { 262 this.methods.add(method.name()); 263 } 264 } 265 } 266 267 @Override 268 public void validate(PortletRequest request) throws PortletException { 269 if (!PortletAnnotationMappingUtils.checkHeaders(this.headers, request)) { 270 throw new PortletRequestBindingException("Header conditions \"" + 271 StringUtils.arrayToDelimitedString(this.headers, ", ") + 272 "\" not met for actual request"); 273 } 274 if (!this.methods.isEmpty()) { 275 if (!(request instanceof ClientDataRequest)) { 276 throw new PortletRequestMethodNotSupportedException(StringUtils.toStringArray(this.methods)); 277 } 278 String method = ((ClientDataRequest) request).getMethod(); 279 if (!this.methods.contains(method)) { 280 throw new PortletRequestMethodNotSupportedException(method, StringUtils.toStringArray(this.methods)); 281 } 282 } 283 } 284 285 @Override 286 public int compareTo(PortletRequestMappingPredicate other) { 287 return (other instanceof SpecialRequestTypePredicate ? -1 : compareParams(other)); 288 } 289 } 290 291 292 private static class MethodLevelMappingPredicate extends AbstractParameterMappingPredicate { 293 294 public MethodLevelMappingPredicate(String[] params) { 295 super(params); 296 } 297 298 @Override 299 public void validate(PortletRequest request) throws PortletException { 300 } 301 302 @Override 303 public int compareTo(PortletRequestMappingPredicate other) { 304 return (other instanceof SpecialRequestTypePredicate ? 1 : compareParams(other)); 305 } 306 } 307 308 309 private static class ActionMappingPredicate extends AbstractParameterMappingPredicate implements SpecialRequestTypePredicate { 310 311 private final String actionName; 312 313 public ActionMappingPredicate(String actionName, String[] params) { 314 super(params); 315 this.actionName = actionName; 316 } 317 318 @Override 319 public boolean match(PortletRequest request) { 320 return (PortletRequest.ACTION_PHASE.equals(request.getAttribute(PortletRequest.LIFECYCLE_PHASE)) && 321 ("".equals(this.actionName) || this.actionName.equals(request.getParameter(ActionRequest.ACTION_NAME))) && 322 super.match(request)); 323 } 324 325 @Override 326 public void validate(PortletRequest request) { 327 } 328 329 @Override 330 public int compareTo(PortletRequestMappingPredicate other) { 331 if (other instanceof TypeLevelMappingPredicate) { 332 return 1; 333 } 334 else if (other instanceof ActionMappingPredicate) { 335 ActionMappingPredicate otherAction = (ActionMappingPredicate) other; 336 boolean hasActionName = "".equals(this.actionName); 337 boolean otherHasActionName = "".equals(otherAction.actionName); 338 if (hasActionName != otherHasActionName) { 339 return (hasActionName ? -1 : 1); 340 } 341 else { 342 return compareParams(otherAction); 343 } 344 } 345 if (other instanceof SpecialRequestTypePredicate) { 346 return this.getClass().getName().compareTo(other.getClass().getName()); 347 } 348 return -1; 349 } 350 } 351 352 353 private static class RenderMappingPredicate extends AbstractParameterMappingPredicate implements SpecialRequestTypePredicate{ 354 355 private final WindowState windowState; 356 357 public RenderMappingPredicate(String windowState, String[] params) { 358 super(params); 359 this.windowState = ("".equals(windowState) ? null : new WindowState(windowState)); 360 } 361 362 @Override 363 public boolean match(PortletRequest request) { 364 return (PortletRequest.RENDER_PHASE.equals(request.getAttribute(PortletRequest.LIFECYCLE_PHASE)) && 365 (this.windowState == null || this.windowState.equals(request.getWindowState())) && 366 super.match(request)); 367 } 368 369 @Override 370 public void validate(PortletRequest request) { 371 } 372 373 @Override 374 public int compareTo(PortletRequestMappingPredicate other) { 375 if (other instanceof TypeLevelMappingPredicate) { 376 return 1; 377 } 378 else if (other instanceof RenderMappingPredicate) { 379 RenderMappingPredicate otherRender = (RenderMappingPredicate) other; 380 boolean hasWindowState = (this.windowState != null); 381 boolean otherHasWindowState = (otherRender.windowState != null); 382 if (hasWindowState != otherHasWindowState) { 383 return (hasWindowState ? -1 : 1); 384 } 385 else { 386 return compareParams(otherRender); 387 } 388 } 389 if (other instanceof SpecialRequestTypePredicate) { 390 return this.getClass().getName().compareTo(other.getClass().getName()); 391 } 392 return -1; 393 } 394 } 395 396 397 private static class ResourceMappingPredicate implements PortletRequestMappingPredicate, SpecialRequestTypePredicate { 398 399 private final String resourceId; 400 401 public ResourceMappingPredicate(String resourceId) { 402 this.resourceId = resourceId; 403 } 404 405 @Override 406 public boolean match(PortletRequest request) { 407 return (PortletRequest.RESOURCE_PHASE.equals(request.getAttribute(PortletRequest.LIFECYCLE_PHASE)) && 408 ("".equals(this.resourceId) || this.resourceId.equals(((ResourceRequest) request).getResourceID()))); 409 } 410 411 @Override 412 public void validate(PortletRequest request) { 413 } 414 415 @Override 416 public int compareTo(PortletRequestMappingPredicate other) { 417 if (other instanceof ResourceMappingPredicate) { 418 boolean hasResourceId = !"".equals(this.resourceId); 419 boolean otherHasResourceId = !"".equals(((ResourceMappingPredicate) other).resourceId); 420 if (hasResourceId != otherHasResourceId) { 421 return (hasResourceId ? -1 : 1); 422 } 423 } 424 if (other instanceof SpecialRequestTypePredicate) { 425 return this.getClass().getName().compareTo(other.getClass().getName()); 426 } 427 return -1; 428 } 429 } 430 431 432 private static class EventMappingPredicate implements PortletRequestMappingPredicate, SpecialRequestTypePredicate { 433 434 private final String eventName; 435 436 public EventMappingPredicate(String eventName) { 437 this.eventName = eventName; 438 } 439 440 @Override 441 public boolean match(PortletRequest request) { 442 if (!PortletRequest.EVENT_PHASE.equals(request.getAttribute(PortletRequest.LIFECYCLE_PHASE))) { 443 return false; 444 } 445 if ("".equals(this.eventName)) { 446 return true; 447 } 448 Event event = ((EventRequest) request).getEvent(); 449 return (this.eventName.equals(event.getName()) || this.eventName.equals(event.getQName().toString())); 450 } 451 452 @Override 453 public void validate(PortletRequest request) { 454 } 455 456 @Override 457 public int compareTo(PortletRequestMappingPredicate other) { 458 if (other instanceof EventMappingPredicate) { 459 boolean hasEventName = !"".equals(this.eventName); 460 boolean otherHasEventName = !"".equals(((EventMappingPredicate) other).eventName); 461 if (hasEventName != otherHasEventName) { 462 return (hasEventName ? -1 : 1); 463 } 464 } 465 if (other instanceof SpecialRequestTypePredicate) { 466 return this.getClass().getName().compareTo(other.getClass().getName()); 467 } 468 return -1; 469 } 470 } 471 472}