001/*
002 * Licensed under the Apache License, Version 2.0 (the "License");
003 * you may not use this file except in compliance with the License.
004 * You may obtain a copy of the License at
005 *
006 *     http://www.apache.org/licenses/LICENSE-2.0
007 *
008 * Unless required by applicable law or agreed to in writing, software
009 * distributed under the License is distributed on an "AS IS" BASIS,
010 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
011 * See the License for the specific language governing permissions and
012 * limitations under the License.
013 */
014package org.gbif.ws.server.processor;
015
016import org.gbif.api.annotation.ParamName;
017
018import java.lang.reflect.Field;
019import java.lang.reflect.Method;
020import java.util.Arrays;
021import java.util.HashMap;
022import java.util.Map;
023import java.util.concurrent.ConcurrentHashMap;
024
025import org.springframework.beans.BeanUtils;
026import org.springframework.beans.factory.annotation.Autowired;
027import org.springframework.core.MethodParameter;
028import org.springframework.web.bind.WebDataBinder;
029import org.springframework.web.bind.annotation.RequestParam;
030import org.springframework.web.context.request.NativeWebRequest;
031import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;
032import org.springframework.web.servlet.mvc.method.annotation.ServletModelAttributeMethodProcessor;
033
034/** Process {@link ParamName}. */
035public class ParamNameProcessor extends ServletModelAttributeMethodProcessor {
036
037  @Autowired private RequestMappingHandlerAdapter requestMappingHandlerAdapter;
038
039  private static final Map<Class<?>, Map<String, String>> PARAM_MAPPINGS_CACHE =
040      new ConcurrentHashMap<>(256);
041  private static final Map<Class<?>, Map<String, String>> METHODS_MAPPINGS_CACHE =
042      new ConcurrentHashMap<>(256);
043
044  public ParamNameProcessor() {
045    super(false);
046  }
047
048  @Override
049  public boolean supportsParameter(MethodParameter parameter) {
050    return parameter.hasParameterAnnotation(RequestParam.class)
051        && !BeanUtils.isSimpleProperty(parameter.getParameterType())
052        && (Arrays.stream(parameter.getParameterType().getDeclaredMethods())
053                .anyMatch(method -> method.getAnnotation(ParamName.class) != null)
054            || Arrays.stream(parameter.getParameterType().getDeclaredFields())
055                .anyMatch(field -> field.getAnnotation(ParamName.class) != null));
056  }
057
058  @Override
059  protected void bindRequestParameters(WebDataBinder binder, NativeWebRequest nativeWebRequest) {
060    Object target = binder.getTarget();
061    Map<String, String> paramMappings = this.getParamMappings(target.getClass());
062    Map<String, String> methodMappings = this.getMethodMappings(target.getClass());
063    ParamNameDataBinder paramNameDataBinder =
064        new ParamNameDataBinder(target, binder.getObjectName(), paramMappings, methodMappings);
065    requestMappingHandlerAdapter.getWebBindingInitializer().initBinder(paramNameDataBinder);
066    super.bindRequestParameters(paramNameDataBinder, nativeWebRequest);
067  }
068
069  /**
070   * Get param mappings. It creates a simple mapping: parameter name -> field name. Cache param
071   * mappings in memory.
072   */
073  private Map<String, String> getParamMappings(Class<?> targetClass) {
074    // first check cache
075    if (PARAM_MAPPINGS_CACHE.containsKey(targetClass)) {
076      return PARAM_MAPPINGS_CACHE.get(targetClass);
077    }
078
079    Map<String, String> paramMappings = new HashMap<>(32);
080
081    // process fields
082    Field[] fields = targetClass.getDeclaredFields();
083    for (Field field : fields) {
084      ParamName paramName = field.getAnnotation(ParamName.class);
085      if (paramName != null && !paramName.value().isEmpty()) {
086        paramMappings.put(paramName.value(), field.getName());
087      }
088    }
089
090    // put them to cache
091    PARAM_MAPPINGS_CACHE.put(targetClass, paramMappings);
092    return paramMappings;
093  }
094
095  /**
096   * Get param mappings. It creates a simple mapping: parameter name -> method name. Cache param
097   * mappings in memory.
098   */
099  private Map<String, String> getMethodMappings(Class<?> targetClass) {
100    // first check cache
101    if (METHODS_MAPPINGS_CACHE.containsKey(targetClass)) {
102      return METHODS_MAPPINGS_CACHE.get(targetClass);
103    }
104
105    Map<String, String> methodMappings = new HashMap<>(32);
106
107    // process methods
108    final Method[] methods = targetClass.getDeclaredMethods();
109    for (Method method : methods) {
110      final ParamName paramName = method.getAnnotation(ParamName.class);
111      if (paramName != null && !paramName.value().isEmpty()) {
112        methodMappings.put(paramName.value(), method.getName());
113      }
114    }
115
116    // put them to cache
117    METHODS_MAPPINGS_CACHE.put(targetClass, methodMappings);
118    return methodMappings;
119  }
120}