/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.shindig.protocol.conversion; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import org.apache.commons.lang.StringUtils; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.lang.reflect.InvocationHandler; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Proxy; import java.lang.reflect.Type; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Set; /** * Filter content of a bean according to fields list. * Fields list should be in lower case. And support sub objects using dot notation. * For example to get only the "name" field of the object in the "view" field, * specify "view.name" (and also specify "view" to get the view itself). * Use "*" to get all fields, or "view.*" all sub fields of view (see tests). * Note that specifying "view" does NOT imply "view.*" and that * specifying "view.*" require specifying "view" in order to get the view itself. * (Note that the processBeanFilter resolve the last limitation) * * Note this code create a new object for each filtered object. * Filtering can be done also using cglib.InterfaceMaker and reflect.Proxy.makeProxyInstance * That results with an object that have same finger print as source, but cannot be cast to it. * * @since 2.0.0 */ public class BeanFilter { public static final String ALL_FIELDS = "*"; public static final String DELIMITER = "."; /** Annotation for required field that should not be filtered */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface Unfiltered {} /** * Create a proxy object that filter object fields according to set of fields. * If a field is not specified in the set, the get method will return null. * (Primitive returned type cannot be filtered) * The filter is done recursively on sub items. * @param data the object to filter * @param fields list of fields to pass through. */ public Object createFilteredBean(Object data, Set<String> fields) { return createFilteredBean(data, fields, ""); } @SuppressWarnings("unchecked") private Object createFilteredBean(Object data, Set<String> fields, String fieldName) { // For null, atomic object or for all fields just return original. if (data == null || fields == null || BeanDelegator.PRIMITIVE_TYPE_CLASSES.contains(data.getClass()) || fields.contains(ALL_FIELDS)) { return data; } // For map, generate a new map with filtered objects if (data instanceof Map<? ,?>) { Map<Object, Object> oldMap = (Map<Object, Object>) data; Map<Object, Object> newMap = Maps.newHashMapWithExpectedSize(oldMap.size()); for (Map.Entry<Object, Object> entry : oldMap.entrySet()) { newMap.put(entry.getKey(), createFilteredBean(entry.getValue(), fields, fieldName)); } return newMap; } // For list, generate a new list of filtered objects if (data instanceof List<?>) { List<Object> oldList = (List<Object>) data; List<Object> newList = Lists.newArrayListWithCapacity(oldList.size()); for (Object entry : oldList) { newList.add(createFilteredBean(entry, fields, fieldName)); } return newList; } // Create a new intercepted object: return Proxy.newProxyInstance( data.getClass().getClassLoader(), data.getClass().getInterfaces(), new FilterInvocationHandler(data, fields, fieldName)); } /** * Invocation handler to filter fields. It return null to fields that are not in the list. * It invokes method on original object. It does not filter primitive types. * And it create bean filter proxy for return objects */ private class FilterInvocationHandler implements InvocationHandler { private final String prefix; private final Set<String> fields; private final Object origData; FilterInvocationHandler(Object origData, Set<String> fields, String fieldName) { this.fields = fields; this.prefix = StringUtils.isEmpty(fieldName) ? "" : fieldName + DELIMITER; this.origData = origData; } public Object invoke(Object data, Method method, Object[] args) { String fieldName = null; Object result = null; if (method.getName().startsWith("get") // Do not filter out primitive types, it will result in NPE && !method.getReturnType().isPrimitive()) { // Look for Required annotation boolean required = (method.getAnnotation(Unfiltered.class) != null); fieldName = prefix + method.getName().substring(3).toLowerCase(); if (!required && !fields.contains(fieldName)) { return null; } } try { result = method.invoke(origData, args); } catch (IllegalArgumentException e) { throw new RuntimeException(e); } catch (IllegalAccessException e) { throw new RuntimeException(e); } catch (InvocationTargetException e) { throw new RuntimeException(e); } if (result != null && fieldName != null // if the request ask for all fields, we don't need to filter them && !fields.contains(fieldName + DELIMITER + ALL_FIELDS)) { return createFilteredBean(result, fields, fieldName); // TODO: Consider improving the above by saving the filtered bean in a local map for reuse // for current use the get is called once, so it would actually create overhead } return result; } } public Set<String> processBeanFields(Collection<String> fields) { ImmutableSet.Builder<String> builder = ImmutableSet.builder(); for (String field : fields) { builder.add(field.toLowerCase()); while (field.contains(DELIMITER)) { field = field.substring(0, field.lastIndexOf(DELIMITER)); builder.add(field.toLowerCase()); } } return builder.build(); } /** * Provide list of all fields for a specific bean * @param bean the class to list fields for * @param depth maximum depth of recursive (mainly for infinite loop protection) */ public List<String> getBeanFields(Class<?> bean, int depth) { List<String> fields = Lists.newLinkedList(); for (Method method : bean.getMethods()) { if (method.getName().startsWith("get")) { String fieldName = method.getName().substring(3); fields.add(fieldName); Class<?> returnType = method.getReturnType(); // Get the type of list: if (List.class.isAssignableFrom(returnType)) { ParameterizedType aType = (ParameterizedType) method.getGenericReturnType(); Type[] parameterArgTypes = aType.getActualTypeArguments(); if (parameterArgTypes.length > 0) { returnType = (Class<?>) parameterArgTypes[0]; } else { returnType = null; } } // Get the type of map value if (Map.class.isAssignableFrom(returnType)) { ParameterizedType aType = (ParameterizedType) method.getGenericReturnType(); Type[] parameterArgTypes = aType.getActualTypeArguments(); if (parameterArgTypes.length > 1) { returnType = (Class<?>) parameterArgTypes[1]; } else { returnType = null; } } // Get member fields and append fields using dot notation if (depth > 1 && returnType != null && !returnType.isPrimitive() && !returnType.isEnum() && !BeanDelegator.PRIMITIVE_TYPE_CLASSES.contains(returnType)) { List<String> subFields = getBeanFields(returnType, depth - 1); for (String field : subFields) { fields.add(fieldName + DELIMITER + field); } } } } return fields; } }