|
| 1 | +/* |
| 2 | + * Copyright 2013 the original author or authors. |
| 3 | + * |
| 4 | + * Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | + * you may not use this file except in compliance with the License. |
| 6 | + * You may obtain a copy of the License at |
| 7 | + * |
| 8 | + * http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | + * |
| 10 | + * Unless required by applicable law or agreed to in writing, software |
| 11 | + * distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | + * See the License for the specific language governing permissions and |
| 14 | + * limitations under the License. |
| 15 | + */ |
| 16 | +package org.springframework.data.web; |
| 17 | + |
| 18 | +import java.beans.PropertyEditorSupport; |
| 19 | +import java.lang.annotation.Annotation; |
| 20 | +import java.lang.reflect.Method; |
| 21 | +import java.util.HashSet; |
| 22 | +import java.util.Set; |
| 23 | + |
| 24 | +import javax.servlet.ServletRequest; |
| 25 | + |
| 26 | +import org.springframework.beans.PropertyValue; |
| 27 | +import org.springframework.beans.PropertyValues; |
| 28 | +import org.springframework.beans.factory.annotation.Qualifier; |
| 29 | +import org.springframework.core.MethodParameter; |
| 30 | +import org.springframework.data.domain.PageRequest; |
| 31 | +import org.springframework.data.domain.Pageable; |
| 32 | +import org.springframework.data.domain.Sort; |
| 33 | +import org.springframework.data.domain.Sort.Direction; |
| 34 | +import org.springframework.validation.DataBinder; |
| 35 | +import org.springframework.web.bind.ServletRequestDataBinder; |
| 36 | +import org.springframework.web.bind.ServletRequestParameterPropertyValues; |
| 37 | +import org.springframework.web.bind.support.WebDataBinderFactory; |
| 38 | +import org.springframework.web.context.request.NativeWebRequest; |
| 39 | +import org.springframework.web.method.support.HandlerMethodArgumentResolver; |
| 40 | +import org.springframework.web.method.support.ModelAndViewContainer; |
| 41 | + |
| 42 | +/** |
| 43 | + * Extracts paging information from web requests and thus allows injecting {@link Pageable} instances into controller |
| 44 | + * methods. Request properties to be parsed can be configured. Default configuration uses request properties beginning |
| 45 | + * with {@link #DEFAULT_PREFIX}{@link #DEFAULT_SEPARATOR}. |
| 46 | + * |
| 47 | + * @since 1.6 |
| 48 | + * @author Oliver Gierke |
| 49 | + */ |
| 50 | +public class PageableHandlerArgumentResolver implements HandlerMethodArgumentResolver { |
| 51 | + |
| 52 | + private static final Pageable DEFAULT_PAGE_REQUEST = new PageRequest(0, 10); |
| 53 | + private static final String DEFAULT_PREFIX = "page"; |
| 54 | + private static final String DEFAULT_SEPARATOR = "."; |
| 55 | + |
| 56 | + private Pageable fallbackPagable = DEFAULT_PAGE_REQUEST; |
| 57 | + private String prefix = DEFAULT_PREFIX; |
| 58 | + private String separator = DEFAULT_SEPARATOR; |
| 59 | + |
| 60 | + /** |
| 61 | + * Setter to configure a fallback instance of {@link Pageable} that is being used to back missing parameters. Defaults |
| 62 | + * to {@link #DEFAULT_PAGE_REQUEST}. |
| 63 | + * |
| 64 | + * @param fallbackPagable the fallbackPagable to set |
| 65 | + */ |
| 66 | + public void setFallbackPagable(Pageable fallbackPagable) { |
| 67 | + this.fallbackPagable = null == fallbackPagable ? DEFAULT_PAGE_REQUEST : fallbackPagable; |
| 68 | + } |
| 69 | + |
| 70 | + /** |
| 71 | + * Setter to configure the prefix of request parameters to be used to retrieve paging information. Defaults to |
| 72 | + * {@link #DEFAULT_PREFIX}. |
| 73 | + * |
| 74 | + * @param prefix the prefix to set |
| 75 | + */ |
| 76 | + public void setPrefix(String prefix) { |
| 77 | + this.prefix = null == prefix ? DEFAULT_PREFIX : prefix; |
| 78 | + } |
| 79 | + |
| 80 | + /** |
| 81 | + * Setter to configure the separator between prefix and actual property value. Defaults to {@link #DEFAULT_SEPARATOR}. |
| 82 | + * |
| 83 | + * @param separator the separator to set. Will default to {@link #DEFAULT_SEPEARATOR} if set to {@literal null}. |
| 84 | + */ |
| 85 | + public void setSeparator(String separator) { |
| 86 | + this.separator = null == separator ? DEFAULT_SEPARATOR : separator; |
| 87 | + } |
| 88 | + |
| 89 | + /* |
| 90 | + * (non-Javadoc) |
| 91 | + * @see org.springframework.web.method.support.HandlerMethodArgumentResolver#supportsParameter(org.springframework.core.MethodParameter) |
| 92 | + */ |
| 93 | + public boolean supportsParameter(MethodParameter parameter) { |
| 94 | + return Pageable.class.equals(parameter.getParameterType()); |
| 95 | + } |
| 96 | + |
| 97 | + /* |
| 98 | + * (non-Javadoc) |
| 99 | + * @see org.springframework.web.method.support.HandlerMethodArgumentResolver#resolveArgument(org.springframework.core.MethodParameter, org.springframework.web.method.support.ModelAndViewContainer, org.springframework.web.context.request.NativeWebRequest, org.springframework.web.bind.support.WebDataBinderFactory) |
| 100 | + */ |
| 101 | + public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer mavContainer, |
| 102 | + NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { |
| 103 | + |
| 104 | + assertPageableUniqueness(methodParameter); |
| 105 | + |
| 106 | + Pageable request = getDefaultFromAnnotationOrFallback(methodParameter); |
| 107 | + ServletRequest servletRequest = (ServletRequest) webRequest.getNativeRequest(); |
| 108 | + PropertyValues propertyValues = new ServletRequestParameterPropertyValues(servletRequest, |
| 109 | + getPrefix(methodParameter), separator); |
| 110 | + |
| 111 | + DataBinder binder = new ServletRequestDataBinder(request); |
| 112 | + |
| 113 | + binder.initDirectFieldAccess(); |
| 114 | + binder.registerCustomEditor(Sort.class, new SortPropertyEditor("sort.dir", propertyValues)); |
| 115 | + binder.bind(propertyValues); |
| 116 | + |
| 117 | + if (request.getPageNumber() > 0) { |
| 118 | + request = new PageRequest(request.getPageNumber() - 1, request.getPageSize(), request.getSort()); |
| 119 | + } |
| 120 | + |
| 121 | + return request; |
| 122 | + } |
| 123 | + |
| 124 | + private Pageable getDefaultFromAnnotationOrFallback(MethodParameter methodParameter) { |
| 125 | + |
| 126 | + // search for PageableDefaults annotation |
| 127 | + for (Annotation annotation : methodParameter.getParameterAnnotations()) { |
| 128 | + if (annotation instanceof PageableDefaults) { |
| 129 | + return getDefaultPageRequestFrom((PageableDefaults) annotation); |
| 130 | + } |
| 131 | + } |
| 132 | + |
| 133 | + // Construct request with fallback request to ensure sensible |
| 134 | + // default values. Create fresh copy as Spring will manipulate the |
| 135 | + // instance under the covers |
| 136 | + return new PageRequest(fallbackPagable.getPageNumber(), fallbackPagable.getPageSize(), fallbackPagable.getSort()); |
| 137 | + } |
| 138 | + |
| 139 | + private static Pageable getDefaultPageRequestFrom(PageableDefaults defaults) { |
| 140 | + |
| 141 | + // +1 is because we substract 1 later |
| 142 | + int defaultPageNumber = defaults.pageNumber() + 1; |
| 143 | + int defaultPageSize = defaults.value(); |
| 144 | + |
| 145 | + if (defaults.sort().length == 0) { |
| 146 | + return new PageRequest(defaultPageNumber, defaultPageSize); |
| 147 | + } |
| 148 | + |
| 149 | + return new PageRequest(defaultPageNumber, defaultPageSize, defaults.sortDir(), defaults.sort()); |
| 150 | + } |
| 151 | + |
| 152 | + /** |
| 153 | + * Resolves the prefix to use to bind properties from. Will prepend a possible {@link Qualifier} if available or |
| 154 | + * return the configured prefix otherwise. |
| 155 | + * |
| 156 | + * @param parameter |
| 157 | + * @return |
| 158 | + */ |
| 159 | + private String getPrefix(MethodParameter parameter) { |
| 160 | + |
| 161 | + for (Annotation annotation : parameter.getParameterAnnotations()) { |
| 162 | + if (annotation instanceof Qualifier) { |
| 163 | + return new StringBuilder(((Qualifier) annotation).value()).append("_").append(prefix).toString(); |
| 164 | + } |
| 165 | + } |
| 166 | + |
| 167 | + return prefix; |
| 168 | + } |
| 169 | + |
| 170 | + /** |
| 171 | + * Asserts uniqueness of all {@link Pageable} parameters of the method of the given {@link MethodParameter}. |
| 172 | + * |
| 173 | + * @param parameter |
| 174 | + */ |
| 175 | + private void assertPageableUniqueness(MethodParameter parameter) { |
| 176 | + |
| 177 | + Method method = parameter.getMethod(); |
| 178 | + |
| 179 | + if (containsMoreThanOnePageableParameter(method)) { |
| 180 | + Annotation[][] annotations = method.getParameterAnnotations(); |
| 181 | + assertQualifiersFor(method.getParameterTypes(), annotations); |
| 182 | + } |
| 183 | + } |
| 184 | + |
| 185 | + /** |
| 186 | + * Returns whether the given {@link Method} has more than one {@link Pageable} parameter. |
| 187 | + * |
| 188 | + * @param method |
| 189 | + * @return |
| 190 | + */ |
| 191 | + private boolean containsMoreThanOnePageableParameter(Method method) { |
| 192 | + |
| 193 | + boolean pageableFound = false; |
| 194 | + |
| 195 | + for (Class<?> type : method.getParameterTypes()) { |
| 196 | + |
| 197 | + if (pageableFound && type.equals(Pageable.class)) { |
| 198 | + return true; |
| 199 | + } |
| 200 | + |
| 201 | + if (type.equals(Pageable.class)) { |
| 202 | + pageableFound = true; |
| 203 | + } |
| 204 | + } |
| 205 | + |
| 206 | + return false; |
| 207 | + } |
| 208 | + |
| 209 | + /** |
| 210 | + * Asserts that every {@link Pageable} parameter of the given parameters carries an {@link Qualifier} annotation to |
| 211 | + * distinguish them from each other. |
| 212 | + * |
| 213 | + * @param parameterTypes |
| 214 | + * @param annotations |
| 215 | + */ |
| 216 | + private void assertQualifiersFor(Class<?>[] parameterTypes, Annotation[][] annotations) { |
| 217 | + |
| 218 | + Set<String> values = new HashSet<String>(); |
| 219 | + |
| 220 | + for (int i = 0; i < annotations.length; i++) { |
| 221 | + |
| 222 | + if (Pageable.class.equals(parameterTypes[i])) { |
| 223 | + |
| 224 | + Qualifier qualifier = findAnnotation(annotations[i]); |
| 225 | + |
| 226 | + if (null == qualifier) { |
| 227 | + throw new IllegalStateException( |
| 228 | + "Ambiguous Pageable arguments in handler method. If you use multiple parameters of type Pageable you need to qualify them with @Qualifier"); |
| 229 | + } |
| 230 | + |
| 231 | + if (values.contains(qualifier.value())) { |
| 232 | + throw new IllegalStateException("Values of the user Qualifiers must be unique!"); |
| 233 | + } |
| 234 | + |
| 235 | + values.add(qualifier.value()); |
| 236 | + } |
| 237 | + } |
| 238 | + } |
| 239 | + |
| 240 | + /** |
| 241 | + * Returns a {@link Qualifier} annotation from the given array of {@link Annotation}s. Returns {@literal null} if the |
| 242 | + * array does not contain a {@link Qualifier} annotation. |
| 243 | + * |
| 244 | + * @param annotations |
| 245 | + * @return |
| 246 | + */ |
| 247 | + private Qualifier findAnnotation(Annotation[] annotations) { |
| 248 | + |
| 249 | + for (Annotation annotation : annotations) { |
| 250 | + if (annotation instanceof Qualifier) { |
| 251 | + return (Qualifier) annotation; |
| 252 | + } |
| 253 | + } |
| 254 | + |
| 255 | + return null; |
| 256 | + } |
| 257 | + |
| 258 | + /** |
| 259 | + * {@link java.beans.PropertyEditor} to create {@link Sort} instances from textual representations. The implementation |
| 260 | + * interprets the string as a comma separated list where the first entry is the sort direction ( {@code asc}, |
| 261 | + * {@code desc}) followed by the properties to sort by. |
| 262 | + * |
| 263 | + * @author Oliver Gierke |
| 264 | + */ |
| 265 | + private static class SortPropertyEditor extends PropertyEditorSupport { |
| 266 | + |
| 267 | + private final String orderProperty; |
| 268 | + private final PropertyValues values; |
| 269 | + |
| 270 | + /** |
| 271 | + * Creates a new {@link SortPropertyEditor}. |
| 272 | + * |
| 273 | + * @param orderProperty |
| 274 | + * @param values |
| 275 | + */ |
| 276 | + public SortPropertyEditor(String orderProperty, PropertyValues values) { |
| 277 | + |
| 278 | + this.orderProperty = orderProperty; |
| 279 | + this.values = values; |
| 280 | + } |
| 281 | + |
| 282 | + /* |
| 283 | + * (non-Javadoc) |
| 284 | + * @see java.beans.PropertyEditorSupport#setAsText(java.lang.String) |
| 285 | + */ |
| 286 | + @Override |
| 287 | + public void setAsText(String text) { |
| 288 | + |
| 289 | + PropertyValue rawOrder = values.getPropertyValue(orderProperty); |
| 290 | + Direction order = null == rawOrder ? Direction.ASC : Direction.fromString(rawOrder.getValue().toString()); |
| 291 | + |
| 292 | + setValue(new Sort(order, text)); |
| 293 | + } |
| 294 | + } |
| 295 | +} |
0 commit comments