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.interceptor;
015
016import org.gbif.api.annotation.EmptyToNull;
017import org.gbif.api.model.collections.Collection;
018import org.gbif.api.model.registry.Dataset;
019
020import java.io.IOException;
021import java.lang.reflect.Type;
022
023import org.apache.commons.beanutils.DynaClass;
024import org.apache.commons.beanutils.DynaProperty;
025import org.apache.commons.beanutils.WrapDynaBean;
026import org.apache.commons.lang3.ObjectUtils;
027import org.slf4j.Logger;
028import org.slf4j.LoggerFactory;
029import org.springframework.core.MethodParameter;
030import org.springframework.http.HttpInputMessage;
031import org.springframework.http.converter.HttpMessageConverter;
032import org.springframework.web.bind.annotation.ControllerAdvice;
033import org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdvice;
034
035/**
036 * An interceptor that will convert all possible empty strings to null. All top level string
037 * properties are handled, as are those of nested objects that are in the GBIF registry model
038 * package. This will recurse only 5 levels deep, to guard against potential circular looping.
039 */
040@SuppressWarnings("NullableProblems")
041@ControllerAdvice
042public class EmptyToNullInterceptor implements RequestBodyAdvice {
043
044  private static final Logger LOG = LoggerFactory.getLogger(EmptyToNullInterceptor.class);
045
046  // only goes 5 levels deep to stop potential circular loops
047  private static final int MAX_RECURSION = 5;
048
049  @Override
050  public boolean supports(
051      MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) {
052    return methodParameter.getMethodAnnotation(EmptyToNull.class) != null
053        || methodParameter.getParameterAnnotation(EmptyToNull.class) != null;
054  }
055
056  @Override
057  public HttpInputMessage beforeBodyRead(
058      HttpInputMessage httpInputMessage,
059      MethodParameter methodParameter,
060      Type type,
061      Class<? extends HttpMessageConverter<?>> aClass)
062      throws IOException {
063    return httpInputMessage;
064  }
065
066  @Override
067  public Object afterBodyRead(
068      Object o,
069      HttpInputMessage httpInputMessage,
070      MethodParameter methodParameter,
071      Type type,
072      Class<? extends HttpMessageConverter<?>> aClass) {
073    trimStringsOf(o);
074    return o;
075  }
076
077  @Override
078  public Object handleEmptyBody(
079      Object o,
080      HttpInputMessage httpInputMessage,
081      MethodParameter methodParameter,
082      Type type,
083      Class<? extends HttpMessageConverter<?>> aClass) {
084    return o;
085  }
086
087  void trimStringsOf(Object target) {
088    trimStringsOf(target, 0);
089  }
090
091  private void trimStringsOf(Object target, int level) {
092    if (target != null && level <= MAX_RECURSION) {
093      LOG.debug("Trimming class: {}", target.getClass());
094
095      WrapDynaBean wrapped = new WrapDynaBean(target);
096      DynaClass dynaClass = wrapped.getDynaClass();
097      for (DynaProperty dynaProp : dynaClass.getDynaProperties()) {
098        if (String.class.isAssignableFrom(dynaProp.getType())) {
099          String prop = dynaProp.getName();
100          String orig = (String) wrapped.get(prop);
101          if (orig != null) {
102            String trimmed = orig.trim().isEmpty() ? null : orig;
103
104            if (ObjectUtils.notEqual(orig, trimmed)) {
105              LOG.debug("Overriding value of [{}] from [{}] to [{}]", prop, orig, trimmed);
106              wrapped.set(prop, trimmed);
107            }
108          }
109        } else {
110          try {
111            // trim everything in the registry model package (assume that Dataset resides in the
112            // correct package here)
113            Object property = wrapped.get(dynaProp.getName());
114            if (property != null
115                && (Dataset.class.getPackage() == property.getClass().getPackage()
116                    || Collection.class.getPackage() == property.getClass().getPackage())) {
117              trimStringsOf(property, level + 1);
118            }
119
120          } catch (IllegalArgumentException e) {
121            // expected for non accessible properties
122          }
123        }
124      }
125    }
126  }
127}