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}