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 * &lt;bean class="org.springframework.web.portlet.mvc.annotation.DefaultAnnotationHandlerMapping"&gt;
063 *   &lt;property name="interceptors"&gt;
064 *     ...
065 *   &lt;/property&gt;
066 * &lt;/bean&gt;</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}