You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@shindig.apache.org by zh...@apache.org on 2010/08/04 04:41:30 UTC

svn commit: r982106 - in /shindig/trunk/java: common/src/main/java/org/apache/shindig/protocol/conversion/ common/src/test/java/org/apache/shindig/protocol/conversion/ gadgets/src/main/java/org/apache/shindig/gadgets/servlet/ gadgets/src/test/java/org/...

Author: zhoresh
Date: Wed Aug  4 02:41:30 2010
New Revision: 982106

URL: http://svn.apache.org/viewvc?rev=982106&view=rev
Log:
Separate GadgetHandler interface from implementation classes
http://codereview.appspot.com/1844046/show

Added:
    shindig/trunk/java/common/src/main/java/org/apache/shindig/protocol/conversion/BeanDelegator.java
    shindig/trunk/java/common/src/main/java/org/apache/shindig/protocol/conversion/BeanFilter.java
    shindig/trunk/java/common/src/test/java/org/apache/shindig/protocol/conversion/BeanDelegatorTest.java
    shindig/trunk/java/common/src/test/java/org/apache/shindig/protocol/conversion/BeanFilterTest.java
    shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/GadgetsHandlerApi.java
Modified:
    shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/GadgetsHandler.java
    shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/GadgetsHandlerTest.java

Added: shindig/trunk/java/common/src/main/java/org/apache/shindig/protocol/conversion/BeanDelegator.java
URL: http://svn.apache.org/viewvc/shindig/trunk/java/common/src/main/java/org/apache/shindig/protocol/conversion/BeanDelegator.java?rev=982106&view=auto
==============================================================================
--- shindig/trunk/java/common/src/main/java/org/apache/shindig/protocol/conversion/BeanDelegator.java (added)
+++ shindig/trunk/java/common/src/main/java/org/apache/shindig/protocol/conversion/BeanDelegator.java Wed Aug  4 02:41:30 2010
@@ -0,0 +1,268 @@
+/*
+ * 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.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Maps;
+
+import org.apache.shindig.common.uri.Uri;
+
+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.List;
+import java.util.Map;
+
+/**
+ * Class to create a delegator (proxy) from an interface to a class.
+ * It is used by the GadgetHandler to provide easy separation from interface
+ * to actual implementation classes.
+ * It uses Java reflection which require the usage of interfaces.
+ * The validate function should be used in the test code to validate
+ * that all API functions are implemented by the actual data, and it will
+ * warn us if actual implementation change and break the API.
+ * Delegation support composition, and will create a proxy for fields according
+ * To table of classes to proxy.
+ */
+public class BeanDelegator {
+
+  /** List of Classes that are considered primitives and are not proxied **/
+  public static final ImmutableSet<Class<?>> PRIMITIVE_TYPE_CLASSES = ImmutableSet.of(
+    String.class, Integer.class, Long.class, Boolean.class, Uri.class);
+
+  /** Map from classes to proxy to the interface they are proxied by */
+  private final Map<Class<?>, Class<?>> delegatedClasses;
+
+  private final Map<Enum<?>, Enum<?>> enumConvertionMap;
+
+  public BeanDelegator(Map<Class<?>, Class<?>> delegatedClasses,
+                       Map<Enum<?>, Enum<?>> enumConvertionMap) {
+    this.delegatedClasses = delegatedClasses;
+    this.enumConvertionMap = enumConvertionMap;
+  }
+
+  /**
+   * Create a proxy for the real object.
+   * @param source item to proxy
+   * @return proxied object according to map of classes to proxy
+   */
+  public Object createDelegator(Object source) {
+    if (source == null || delegatedClasses == null || delegatedClasses.size() == 0) {
+      return null;
+    }
+
+    // For enum, return the converted enum
+    if (source instanceof Enum<?> && delegatedClasses.containsKey(source.getClass())) {
+      return convertEnum((Enum<?>) source);
+    }
+
+    // Proxy each item in a map (map key is not proxied)
+    if (source instanceof Map<?, ?>) {
+      Map<?, ?> mapSource = (Map<?, ?>) source;
+      if (mapSource.size() > 0 && delegatedClasses.containsKey(
+          mapSource.values().iterator().next().getClass())) {
+        // Convert Map:
+        ImmutableMap.Builder<Object, Object> mapBuilder = ImmutableMap.builder();
+        for (Map.Entry<?, ?> entry : mapSource.entrySet()) {
+          mapBuilder.put(entry.getKey(), createDelegator(entry.getValue()));
+        }
+        return mapBuilder.build();
+      } else {
+        return source;
+      }
+    }
+
+    // Proxy each item in a list
+    if (source instanceof List<?>) {
+      List<?> listSource = (List<?>) source;
+      if (listSource.size() > 0 && delegatedClasses.containsKey(
+        listSource.get(0).getClass())) {
+        // Convert Map:
+        ImmutableList.Builder<Object> listBuilder = ImmutableList.builder();
+        for (Object entry : listSource) {
+          listBuilder.add(createDelegator(entry));
+        }
+        return listBuilder.build();
+      } else {
+        return source;
+      }
+    }
+
+    if (delegatedClasses.containsKey(source.getClass())) {
+      Class<?> apiInterface = delegatedClasses.get(source.getClass());
+
+      return Proxy.newProxyInstance( apiInterface.getClassLoader(),
+          new Class[] { apiInterface }, new DelegateInvocationHandler(source));
+    }
+    return source;
+  }
+
+  public Enum<?> convertEnum(Enum<?> value) {
+    if (enumConvertionMap.containsKey(value)) {
+      return enumConvertionMap.get(value);
+    }
+    throw new UnsupportedOperationException("Unknown enum value " + value.toString());
+  }
+
+  protected class DelegateInvocationHandler implements InvocationHandler {
+    /** Proxied object */
+    private final Object source;
+
+    public DelegateInvocationHandler(Object source) {
+      Preconditions.checkNotNull(source);
+      this.source = source;
+    }
+
+    /**
+     * Proxy the interface function to the source object
+     * @throw UnsupportedOperationException if method is not supported by source
+     */
+    public Object invoke(Object proxy, Method method, Object[] args) {
+      Class<?> sourceClass = source.getClass();
+      try {
+        Method sourceMethod = sourceClass.getMethod(
+            method.getName(), method.getParameterTypes());
+        Object result = sourceMethod.invoke(source, args);
+        return createDelegator(result);
+      } catch (NoSuchMethodException e) {
+        // Will throw unsupported method below
+      } catch (IllegalArgumentException e) {
+        // Will throw unsupported method below
+      } catch (IllegalAccessException e) {
+        // Will throw unsupported method below
+      } catch (InvocationTargetException e) {
+        // Will throw unsupported method below
+      }
+      throw new UnsupportedOperationException("Unsupported function: " + method.getName());
+    }
+  }
+
+  /**
+   * Validate all proxied classes to see that all required functions are implemented.
+   * Throws exception if failed validation.
+   * @throws SecurityException
+   * @throws NoSuchMethodException
+   * @throws NoSuchFieldException
+   */
+  public void validate() throws SecurityException, NoSuchMethodException, NoSuchFieldException {
+    for (Map.Entry<Class<?>, Class<?>> entry : delegatedClasses.entrySet()) {
+      if (!entry.getKey().isEnum()) {
+        validate(entry.getKey(), entry.getValue());
+      }
+    }
+  }
+
+  public void validate(Class<?> dataClass, Class<?> interfaceClass)
+      throws SecurityException, NoSuchMethodException, NoSuchFieldException {
+    for (Method method : interfaceClass.getMethods()) {
+      Method dataMethod = dataClass.getMethod(method.getName(), method.getParameterTypes());
+      if (dataMethod == null) {
+        throw new NoSuchMethodException("Method " + method.getName()
+            + " is not implemented by " + dataClass.getName());
+      }
+      if (!validateTypes(dataMethod.getGenericReturnType(), method.getGenericReturnType())) {
+        throw new NoSuchMethodException("Method " + method.getName()
+          + " has wrong return type by " + dataClass.getName());
+      }
+    }
+  }
+
+  private boolean validateTypes(Type dataType, Type interfaceType)
+      throws NoSuchFieldException {
+
+    // Handle Map and List parameterized types
+    if (dataType instanceof ParameterizedType) {
+      ParameterizedType dataParamType = (ParameterizedType) dataType;
+      ParameterizedType interfaceParamType = (ParameterizedType) interfaceType;
+
+      if (List.class.isAssignableFrom((Class<?>) dataParamType.getRawType()) &&
+          List.class.isAssignableFrom((Class<?>) interfaceParamType.getRawType())) {
+
+        dataType = dataParamType.getActualTypeArguments()[0];
+        interfaceType = interfaceParamType.getActualTypeArguments()[0];
+        return validateTypes(dataType, interfaceType);
+      }
+      if (Map.class.isAssignableFrom((Class<?>) dataParamType.getRawType()) &&
+          Map.class.isAssignableFrom((Class<?>) interfaceParamType.getRawType())) {
+        Type dataKeyType = dataParamType.getActualTypeArguments()[0];
+        Type interfaceKeyType = interfaceParamType.getActualTypeArguments()[0];
+        if (dataKeyType != interfaceKeyType || !PRIMITIVE_TYPE_CLASSES.contains(dataKeyType)) {
+          return false;
+        }
+        dataType = dataParamType.getActualTypeArguments()[1];
+        interfaceType = interfaceParamType.getActualTypeArguments()[1];
+        return validateTypes(dataType, interfaceType);
+      }
+      // Only support Map and List generics
+      return false;
+    }
+
+    // Primitive types
+    if (dataType == interfaceType) {
+      if (!PRIMITIVE_TYPE_CLASSES.contains(dataType) && !((Class<?>) dataType).isPrimitive()) {
+        return false;
+      }
+      return true;
+    }
+
+    // Check all enum values are accounted for
+    Class<?> dataClass = (Class<?>)dataType;
+    if (dataClass.isEnum()) {
+      for (Object f : dataClass.getEnumConstants()) {
+        if (!enumConvertionMap.containsKey(f) ||
+            enumConvertionMap.get(f).getClass() != interfaceType) {
+          throw new NoSuchFieldException("Enum " + dataClass.getName()
+            + " don't have mapping for value " + f.toString());
+        }
+      }
+    }
+    return (delegatedClasses.get(dataType) == interfaceType);
+  }
+
+
+  /**
+   * Utility function to auto generate mapping between two enums that have same values (toString)
+   * All values in the sourceEnum must have values in targetEnum,
+   *  otherwise {@link RuntimeException} is thrown
+   */
+  public static Map<Enum<?>, Enum<?>> createDefaultEnumMap(
+      Class<? extends Enum<?>> sourceEnum, Class<? extends Enum<?>> targetEnum) {
+   Map<String, Enum<?>> values2Map = Maps.newHashMap();
+   for (Object val2 : targetEnum.getEnumConstants()) {
+     values2Map.put(val2.toString(), (Enum<?>) val2);
+   }
+   Enum<?>[] values1 = sourceEnum.getEnumConstants();
+   ImmutableMap.Builder<Enum<?>, Enum<?>> mapBuilder = ImmutableMap.builder();
+   for (Enum<?> val1 : sourceEnum.getEnumConstants()) {
+     if (values2Map.containsKey(val1.toString())) {
+       mapBuilder.put(val1, values2Map.get(val1.toString()));
+     } else {
+       throw new RuntimeException("Missing enum value " + val1.toString()
+           + " for enum " + targetEnum.getName());
+     }
+   }
+   return mapBuilder.build();
+  }
+}

Added: shindig/trunk/java/common/src/main/java/org/apache/shindig/protocol/conversion/BeanFilter.java
URL: http://svn.apache.org/viewvc/shindig/trunk/java/common/src/main/java/org/apache/shindig/protocol/conversion/BeanFilter.java?rev=982106&view=auto
==============================================================================
--- shindig/trunk/java/common/src/main/java/org/apache/shindig/protocol/conversion/BeanFilter.java (added)
+++ shindig/trunk/java/common/src/main/java/org/apache/shindig/protocol/conversion/BeanFilter.java Wed Aug  4 02:41:30 2010
@@ -0,0 +1,219 @@
+/*
+ * 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.Annotation;
+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.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.
+ */
+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 Required {}
+
+  /**
+   * 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(Required.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(Set<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;
+  }
+
+}

Added: shindig/trunk/java/common/src/test/java/org/apache/shindig/protocol/conversion/BeanDelegatorTest.java
URL: http://svn.apache.org/viewvc/shindig/trunk/java/common/src/test/java/org/apache/shindig/protocol/conversion/BeanDelegatorTest.java?rev=982106&view=auto
==============================================================================
--- shindig/trunk/java/common/src/test/java/org/apache/shindig/protocol/conversion/BeanDelegatorTest.java (added)
+++ shindig/trunk/java/common/src/test/java/org/apache/shindig/protocol/conversion/BeanDelegatorTest.java Wed Aug  4 02:41:30 2010
@@ -0,0 +1,205 @@
+/*
+ * 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.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+
+import junit.framework.Assert;
+
+import org.apache.shindig.protocol.conversion.BeanFilter.Required;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.List;
+import java.util.Map;
+
+public class BeanDelegatorTest extends Assert {
+
+  // Note, this classes also used by the BeanFilter tests
+  public static interface SimpleBeanInterface {
+    public int getI();
+    public SimpleBeanInterface setI(int i);
+    public String getS();
+    // Test list conversions:
+    public List<String> getList();
+    public List<SimpleBeanInterface> getBeanList();
+    // Test Map conversion
+    public Map<String, String> getMap();
+    public Map<String, SimpleBeanInterface> getBeanMap();
+    // Test error cases
+    public String getUnknown(); // delegated class doesn't have this
+    public int getWrongType(); // delegated class return different type
+    public String getPrivateData(); // delegated class method is private
+
+    // Test enum
+    public enum Style { A, B; }
+    public Style getStyle();
+
+    // Test of required
+    @Required
+    public String getRequired();
+  }
+
+  public static class SimpleBean {
+    private int i;
+    private String s;
+    private List<String> l;
+    private List<SimpleBean> beanList;
+    private Map<String, String> stringMap;
+    private Map<String, SimpleBean> beanMap;
+
+    public int getI() { return i; }
+    public SimpleBean setI(int ni) { i = ni; return this; }
+
+    public String getS() { return s; }
+    public SimpleBean setS(String ns) { s = ns; return this; }
+
+    public List<String> getList() { return l; }
+    public SimpleBean setList(List<String> nl) { l = nl; return this; }
+
+    public List<SimpleBean> getBeanList() { return beanList; }
+    public SimpleBean setBeanList(List<SimpleBean> nl) { beanList = nl; return this; }
+
+    public Map<String, String> getMap() { return stringMap; }
+    public SimpleBean setMap(Map<String, String> nm) { stringMap = nm; return this; }
+
+    public Map<String, SimpleBean> getBeanMap() { return beanMap; }
+    public SimpleBean setBeanMap(Map<String, SimpleBean> nm) { beanMap = nm; return this; }
+
+    public String getWrongType() { return "this is string"; }
+
+    @SuppressWarnings("unused")
+    private String getPrivateData() { return "this is private"; }
+
+    // Enum data:
+    public enum RealStyle { R_A, R_B; }
+    RealStyle style;
+    public RealStyle getStyle() { return style; }
+    public SimpleBean setStyle(RealStyle style) { this.style = style; return this; }
+
+    // Test of required
+    public String getRequired() { return "required"; }
+  }
+
+  private BeanDelegator beanDelegator;
+  private SimpleBean source;
+  private SimpleBeanInterface proxy;
+
+  public static BeanDelegator createSimpleDelegator() {
+    BeanDelegator beanDelegator = new BeanDelegator(
+        ImmutableMap.<Class<?>, Class<?>>of(SimpleBean.class, SimpleBeanInterface.class,
+            SimpleBean.RealStyle.class, SimpleBeanInterface.Style.class),
+        ImmutableMap.<Enum<?>, Enum<?>>of(SimpleBean.RealStyle.R_A, SimpleBeanInterface.Style.A,
+            SimpleBean.RealStyle.R_B, SimpleBeanInterface.Style.B));
+    return beanDelegator;
+  }
+
+  @Before
+  public void setUp() {
+    beanDelegator = createSimpleDelegator();
+    source = new SimpleBean();
+    proxy = (SimpleBeanInterface) beanDelegator.createDelegator(source);
+  }
+
+  @Test
+  public void testSimpleBean() {
+    String s = "test";
+    source.setS(s);
+    assertEquals(s, proxy.getS());
+
+    proxy.setI(5);
+    assertEquals(5, proxy.getI());
+    assertEquals(5, source.getI());
+
+    source.setStyle(SimpleBean.RealStyle.R_A);
+    assertEquals(SimpleBeanInterface.Style.A, proxy.getStyle());
+  }
+
+  @Test(expected = UnsupportedOperationException.class)
+  public void testUnimplementedFunction() {
+    proxy.getUnknown();
+  }
+
+  @Test(expected = ClassCastException.class)
+  public void testWrontType() {
+    proxy.getWrongType();
+  }
+
+  @Test(expected = UnsupportedOperationException.class)
+  public void testPrivateAccess() {
+    proxy.getPrivateData();
+  }
+
+  @Test
+  public void testStringList() {
+    assertNull(proxy.getList());
+    List<String> stringList = ImmutableList.of("item1", "item2");
+    source.setList(stringList);
+    assertEquals(stringList, proxy.getList());
+    stringList = ImmutableList.of();
+    source.setList(stringList);
+    assertEquals(stringList, proxy.getList());
+  }
+
+  @Test
+  public void testBeanList() {
+    List<SimpleBean> beanList = ImmutableList.of();
+    source.setBeanList(beanList);
+    assertEquals(beanList, proxy.getBeanList());
+
+    SimpleBean item = new SimpleBean().setS("item");
+    beanList = ImmutableList.of(item);
+    source.setBeanList(beanList);
+    List<SimpleBeanInterface> interList = proxy.getBeanList();
+    assertEquals(1, interList.size());
+    assertEquals(item.getS(), interList.get(0).getS());
+  }
+
+  @Test
+  public void testStringMap() {
+    assertNull(proxy.getMap());
+    Map<String, String> stringMap = ImmutableMap.of("item1", "v1", "item2", "v2");
+    source.setMap(stringMap);
+    assertEquals(stringMap, proxy.getMap());
+    stringMap = ImmutableMap.of();
+    source.setMap(stringMap);
+    assertEquals(stringMap, proxy.getMap());
+  }
+
+  @Test
+  public void testBeanMap() {
+    Map<String, SimpleBean> beanMap = ImmutableMap.of();
+    source.setBeanMap(beanMap);
+    assertEquals(beanMap, proxy.getBeanMap());
+
+    SimpleBean item = new SimpleBean().setS("item");
+    beanMap = ImmutableMap.of("item", item);
+    source.setBeanMap(beanMap);
+    Map<String, SimpleBeanInterface> interMap = proxy.getBeanMap();
+    assertEquals(1, interMap.size());
+    assertEquals(item.getS(), interMap.get("item").getS());
+  }
+
+  // Make sure validate will actually fail
+  @Test(expected = NoSuchMethodException.class)
+  public void tesValidate() throws Exception {
+    beanDelegator.validate();
+  }
+}

Added: shindig/trunk/java/common/src/test/java/org/apache/shindig/protocol/conversion/BeanFilterTest.java
URL: http://svn.apache.org/viewvc/shindig/trunk/java/common/src/test/java/org/apache/shindig/protocol/conversion/BeanFilterTest.java?rev=982106&view=auto
==============================================================================
--- shindig/trunk/java/common/src/test/java/org/apache/shindig/protocol/conversion/BeanFilterTest.java (added)
+++ shindig/trunk/java/common/src/test/java/org/apache/shindig/protocol/conversion/BeanFilterTest.java Wed Aug  4 02:41:30 2010
@@ -0,0 +1,178 @@
+/*
+ * 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.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+
+import org.apache.shindig.protocol.conversion.BeanDelegatorTest.SimpleBean;
+import org.apache.shindig.protocol.conversion.BeanDelegatorTest.SimpleBeanInterface;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+public class BeanFilterTest extends Assert {
+
+  private BeanFilter beanFilter;
+  private BeanDelegator beanDelegator;
+
+  @Before
+  public void setUp() {
+    beanFilter = new BeanFilter();
+    beanDelegator = BeanDelegatorTest.createSimpleDelegator();
+  }
+
+  @Test
+  public void testNull() throws Exception {
+    assertNull(beanFilter.createFilteredBean(null, ImmutableSet.<String>of("s")));
+  }
+
+  @Test
+  public void testSimple() throws Exception {
+    String data = "data";
+
+    String newData = (String) beanFilter.createFilteredBean(data, null);
+    assertSame(data, newData);
+  }
+
+  @Test
+  public void testInt() throws Exception {
+    SimpleBean data = new SimpleBean().setI(5);
+    SimpleBeanInterface dataBean = (SimpleBeanInterface) beanDelegator.createDelegator(data);
+
+    SimpleBeanInterface newData = (SimpleBeanInterface) beanFilter.createFilteredBean(
+        dataBean, ImmutableSet.<String>of("i"));
+    assertEquals(5, newData.getI());
+
+    newData = (SimpleBeanInterface) beanFilter.createFilteredBean(
+        dataBean, ImmutableSet.<String>of("s"));
+    // Filter is ignored for primitive types:
+    assertEquals(5, newData.getI());
+  }
+
+  @Test
+  public void testString() throws Exception {
+    SimpleBean data = new SimpleBean().setS("data");
+    SimpleBeanInterface dataBean = (SimpleBeanInterface) beanDelegator.createDelegator(data);
+
+    SimpleBeanInterface newData = (SimpleBeanInterface) beanFilter.createFilteredBean(
+        dataBean, ImmutableSet.<String>of("s"));
+    assertEquals("data", newData.getS());
+    newData = (SimpleBeanInterface) beanFilter.createFilteredBean(
+        dataBean, ImmutableSet.<String>of("i"));
+    assertNull("S is filtered out", newData.getS());
+    assertNotNull("Required field", newData.getRequired());
+  }
+
+  @Test
+  public void testList() throws Exception {
+    SimpleBean data = new SimpleBean().setList(ImmutableList.<String>of("d1", "d2"));
+    SimpleBeanInterface dataBean = (SimpleBeanInterface) beanDelegator.createDelegator(data);
+
+    SimpleBeanInterface newData = (SimpleBeanInterface) beanFilter.createFilteredBean(
+      dataBean, ImmutableSet.<String>of("s"));
+    assertEquals(null, newData.getList());
+
+    newData = (SimpleBeanInterface) beanFilter.createFilteredBean(
+        dataBean, ImmutableSet.<String>of("list"));
+    assertArrayEquals(data.getList().toArray(), newData.getList().toArray());
+  }
+
+  @Test
+  public void testMap() throws Exception {
+    List<String> list = ImmutableList.<String>of("test");
+    SimpleBean data = new SimpleBean().setS("Main").setBeanMap(
+        ImmutableMap.<String, SimpleBean>of( "s1", new SimpleBean().setS("sub1").setList(list),
+          "s2", new SimpleBean().setS("sub2").setList(list).setBeanMap(
+              ImmutableMap.of("s2s1", new SimpleBean().setS("sub2-sub1"))
+        )));
+    SimpleBeanInterface dataBean = (SimpleBeanInterface) beanDelegator.createDelegator(data);
+
+    SimpleBeanInterface newData = (SimpleBeanInterface) beanFilter.createFilteredBean(dataBean,
+        ImmutableSet.<String>of("beanmap"));
+    assertEquals(2, newData.getBeanMap().size());
+    assertEquals(null, newData.getBeanMap().get("s1").getS());
+
+    newData = (SimpleBeanInterface) beanFilter.createFilteredBean(dataBean,
+      ImmutableSet.<String>of("beanmap", "beanmap.s"));
+    assertNotSame(dataBean.getBeanMap().getClass(), newData.getBeanMap().getClass());
+    assertEquals(2, newData.getBeanMap().size());
+    assertEquals("sub1", newData.getBeanMap().get("s1").getS());
+    assertNull("List is filtered out", newData.getBeanMap().get("s1").getList());
+
+    newData = (SimpleBeanInterface) beanFilter.createFilteredBean(dataBean,
+      ImmutableSet.<String>of("beanmap", "beanmap.*"));
+    // Verify filter is a simple pass through.
+    // can only check class since each time different delegator is created
+    assertSame(dataBean.getBeanMap().getClass(), newData.getBeanMap().getClass());
+
+    newData = (SimpleBeanInterface) beanFilter.createFilteredBean(dataBean,
+        ImmutableSet.<String>of("beanmap", "beanmap.beanmap", "beanmap.beanmap.s"));
+    assertEquals(2, newData.getBeanMap().size());
+    Map<String, SimpleBeanInterface> subSubMap = newData.getBeanMap().get("s2").getBeanMap();
+    assertEquals(1, subSubMap.size());
+    assertEquals("sub2-sub1", subSubMap.get("s2s1").getS());
+    assertNull("list is filtered", subSubMap.get("s2s1").getList());
+
+    newData = (SimpleBeanInterface) beanFilter.createFilteredBean(dataBean,
+        ImmutableSet.<String>of("beanmap", "beanmap.beanmap", "beanmap.beanmap.*"));
+    assertEquals(2, newData.getBeanMap().size());
+    assertNotSame(dataBean.getBeanMap().getClass(), newData.getBeanMap().getClass());
+    assertSame(data.getBeanMap().get("s2").getBeanMap().getClass(),
+        newData.getBeanMap().get("s2").getBeanMap().getClass());
+  }
+
+  @Test
+  public void testProcessFields() {
+    Set<String> srcFields = ImmutableSet.of("A", "b", "c.d.e.f", "Case", "cAse", "CASE");
+    Set<String> newFields = beanFilter.processBeanFields(srcFields);
+    assertEquals(7, newFields.size());
+    assertTrue(newFields.contains("a"));
+    assertTrue(newFields.contains("b"));
+    assertTrue(newFields.contains("c"));
+    assertTrue(newFields.contains("c.d"));
+    assertTrue(newFields.contains("c.d.e"));
+    assertTrue(newFields.contains("c.d.e.f"));
+    assertTrue(newFields.contains("case"));
+  }
+
+  @Test
+  public void testListFields() {
+    List<String> fields = beanFilter.getBeanFields(SimpleBeanInterface.class, 3);
+    assertTrue(fields.contains("Map"));
+    assertTrue(fields.contains("I"));
+    assertTrue(fields.contains("S"));
+    assertTrue(fields.contains("Style"));
+    assertTrue(fields.contains("List"));
+    assertTrue(fields.contains("BeanList.List"));
+    assertTrue(fields.contains("Map"));
+    assertTrue(fields.contains("BeanMap.List"));
+    assertTrue(fields.contains("BeanMap.BeanMap.BeanMap"));
+    assertFalse(fields.contains("BeanMap.BeanMap.BeanMap.BeanMap"));
+    assertEquals(77, fields.size());
+    // If failed use next prints to verify and fix
+    // System.out.println(fields.size());
+    // System.out.println(fields.toString());
+  }
+}

Modified: shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/GadgetsHandler.java
URL: http://svn.apache.org/viewvc/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/GadgetsHandler.java?rev=982106&r1=982105&r2=982106&view=diff
==============================================================================
--- shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/GadgetsHandler.java (original)
+++ shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/GadgetsHandler.java Wed Aug  4 02:41:30 2010
@@ -31,16 +31,21 @@ import org.apache.shindig.gadgets.Gadget
 import org.apache.shindig.gadgets.GadgetContext;
 import org.apache.shindig.gadgets.RenderingContext;
 import org.apache.shindig.gadgets.process.Processor;
+import org.apache.shindig.gadgets.spec.Feature;
 import org.apache.shindig.gadgets.spec.GadgetSpec;
+import org.apache.shindig.gadgets.spec.LinkSpec;
 import org.apache.shindig.gadgets.spec.ModulePrefs;
 import org.apache.shindig.gadgets.spec.UserPref;
 import org.apache.shindig.gadgets.spec.View;
+import org.apache.shindig.gadgets.spec.UserPref.EnumValuePair;
 import org.apache.shindig.gadgets.uri.IframeUriManager;
 import org.apache.shindig.protocol.BaseRequestItem;
 import org.apache.shindig.protocol.Operation;
 import org.apache.shindig.protocol.ProtocolException;
 import org.apache.shindig.protocol.RequestItem;
 import org.apache.shindig.protocol.Service;
+import org.apache.shindig.protocol.conversion.BeanDelegator;
+import org.apache.shindig.protocol.conversion.BeanFilter;
 
 import java.util.Locale;
 import java.util.Map;
@@ -60,59 +65,98 @@ public class GadgetsHandler {
   @VisibleForTesting
   static final String FAILURE_TOKEN = "Failed to get gadget token.";
 
-  private static final Set<String> ALL_METADATA_FIELDS = ImmutableSet.of(
-      "iframeUrl", "userPrefs", "modulePrefs", "views", "views.name", "views.type",
-      "views.type", "views.href", "views.quirks", "views.content",
-      "views.preferredHeight", "views.preferredWidth",
-      "views.needsUserPrefsSubstituted", "views.attributes");
-  private static final Set<String> DEFAULT_METADATA_FIELDS = ImmutableSet.of(
-      "iframeUrl", "userPrefs", "modulePrefs", "views");
+  private static final Set<String> DEFAULT_METADATA_FIELDS =
+      ImmutableSet.of("iframeUrl", "userPrefs.*", "modulePrefs.*", "views.*", "token");
 
   protected final ExecutorService executor;
   protected final Processor processor;
   protected final IframeUriManager iframeUriManager;
   protected final SecurityTokenCodec securityTokenCodec;
 
+  protected final BeanDelegator beanDelegator;
+  protected final BeanFilter beanFilter;
+
+  // Map shindig data class to API interfaces
+  @VisibleForTesting
+  static final Map<Class<?>, Class<?>> apiClasses =
+      new ImmutableMap.Builder<Class<?>, Class<?>>()
+          .put(BaseResponseData.class, GadgetsHandlerApi.BaseResponse.class)
+          .put(MetadataResponseData.class, GadgetsHandlerApi.MetadataResponse.class)
+          .put(TokenResponseData.class, GadgetsHandlerApi.TokenResponse.class)
+          .put(View.class, GadgetsHandlerApi.View.class)
+          .put(UserPref.class, GadgetsHandlerApi.UserPref.class)
+          .put(EnumValuePair.class, GadgetsHandlerApi.EnumValuePair.class)
+          .put(ModulePrefs.class, GadgetsHandlerApi.ModulePrefs.class)
+          .put(Feature.class, GadgetsHandlerApi.Feature.class)
+          .put(LinkSpec.class, GadgetsHandlerApi.LinkSpec.class)
+          // Enums
+          .put(View.ContentType.class, GadgetsHandlerApi.ViewContentType.class)
+          .put(UserPref.DataType.class, GadgetsHandlerApi.UserPrefDataType.class)
+          .build();
+
+  // Provide mapping for internal enums to api enums
+  @VisibleForTesting
+  static final Map<Enum<?>, Enum<?>> enumConversionMap =
+      new ImmutableMap.Builder<Enum<?>, Enum<?>>()
+          // View.ContentType mapping
+          .putAll(BeanDelegator.createDefaultEnumMap(View.ContentType.class,
+              GadgetsHandlerApi.ViewContentType.class))
+          // UserPref.DataType mapping
+          .putAll(BeanDelegator.createDefaultEnumMap(UserPref.DataType.class,
+              GadgetsHandlerApi.UserPrefDataType.class))
+          .build();
+
   @Inject
-  public GadgetsHandler(
-      ExecutorService executor,
-      Processor processor,
-      IframeUriManager iframeUriManager,
-      SecurityTokenCodec securityTokenCodec) {
+  public GadgetsHandler(ExecutorService executor, Processor processor,
+      IframeUriManager iframeUriManager, SecurityTokenCodec securityTokenCodec,
+      BeanFilter beanFilter) {
     this.executor = executor;
     this.processor = processor;
     this.iframeUriManager = iframeUriManager;
     this.securityTokenCodec = securityTokenCodec;
+    this.beanFilter = beanFilter;
+
+    // TODO: maybe make this injectable
+    this.beanDelegator = new BeanDelegator(apiClasses, enumConversionMap);
   }
 
   @Operation(httpMethods = {"POST", "GET"}, path = "metadata.get")
-  public Map<String, MetadataResponse> metadata(BaseRequestItem request)
+  public Map<String, GadgetsHandlerApi.MetadataResponse> metadata(BaseRequestItem request)
       throws ProtocolException {
-    return new AbstractExecutor<MetadataResponse>() {
+    return new AbstractExecutor<GadgetsHandlerApi.MetadataResponse>() {
       @Override
-      protected Callable<MetadataResponse> createJob(String url, BaseRequestItem request) {
+      protected Callable<GadgetsHandlerApi.MetadataResponse> createJob(String url,
+          BaseRequestItem request) {
         return createMetadataJob(url, request);
       }
     }.execute(request);
   }
 
   @Operation(httpMethods = {"POST", "GET"}, path = "token.get")
-  public Map<String, TokenResponse> token(BaseRequestItem request)
+  public Map<String, GadgetsHandlerApi.TokenResponse> token(BaseRequestItem request)
       throws ProtocolException {
-    return new AbstractExecutor<TokenResponse>() {
+    return new AbstractExecutor<GadgetsHandlerApi.TokenResponse>() {
       @Override
-      protected Callable<TokenResponse> createJob(String url, BaseRequestItem request) {
+      protected Callable<GadgetsHandlerApi.TokenResponse> createJob(String url,
+          BaseRequestItem request) {
         return createTokenJob(url, request);
       }
     }.execute(request);
   }
 
-  @Operation(httpMethods = "GET", path="/@metadata.supportedFields")
+  @Operation(httpMethods = "GET", path = "/@metadata.supportedFields")
   public Set<String> supportedFields(RequestItem request) {
-    return ALL_METADATA_FIELDS;
+    return ImmutableSet.copyOf(beanFilter
+        .getBeanFields(GadgetsHandlerApi.MetadataResponse.class, 5));
+  }
+
+  @Operation(httpMethods = "GET", path = "/@token.supportedFields")
+  public Set<String> tokenSupportedFields(RequestItem request) {
+    return ImmutableSet.copyOf(
+        beanFilter.getBeanFields(GadgetsHandlerApi.TokenResponse.class, 5));
   }
 
-  private abstract class AbstractExecutor<R extends BaseResponse> {
+  private abstract class AbstractExecutor<R extends GadgetsHandlerApi.BaseResponse> {
     @SuppressWarnings("unchecked")
     public Map<String, R> execute(BaseRequestItem request) {
       Set<String> gadgetUrls = ImmutableSet.copyOf(request.getListParameter("ids"));
@@ -133,20 +177,19 @@ public class GadgetsHandler {
           response = completionService.take().get();
           builder.put(response.getUrl(), response);
         } catch (InterruptedException e) {
-          throw new ProtocolException(
-              HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
+          throw new ProtocolException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
               "Processing interrupted.", e);
         } catch (ExecutionException e) {
           if (!(e.getCause() instanceof RpcException)) {
-            throw new ProtocolException(
-                HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
+            throw new ProtocolException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
                 "Processing error.", e);
           }
           RpcException cause = (RpcException) e.getCause();
           GadgetContext context = cause.getContext();
           if (context != null) {
             String url = context.getUrl().toString();
-            R errorResponse = (R) new BaseResponse(url, cause.getMessage());
+            R errorResponse =
+                (R) beanDelegator.createDelegator(new BaseResponseData(url, cause.getMessage()));
             builder.put(url, errorResponse);
           }
         }
@@ -158,17 +201,22 @@ public class GadgetsHandler {
   }
 
   // Hook to override in sub-class.
-  protected Callable<MetadataResponse> createMetadataJob(String url, BaseRequestItem request) {
+  protected Callable<GadgetsHandlerApi.MetadataResponse> createMetadataJob(String url,
+      BaseRequestItem request) {
     final GadgetContext context = new MetadataGadgetContext(url, request);
-    final Set<String> fields = request.getFields(DEFAULT_METADATA_FIELDS);
-    return new Callable<MetadataResponse>() {
-      public MetadataResponse call() throws Exception {
+    final Set<String> fields =
+        beanFilter.processBeanFields(request.getFields(DEFAULT_METADATA_FIELDS));
+    return new Callable<GadgetsHandlerApi.MetadataResponse>() {
+      public GadgetsHandlerApi.MetadataResponse call() throws Exception {
         try {
           Gadget gadget = processor.process(context);
-          String iframeUrl = fields.contains("iframeUrl")
-              ? iframeUriManager.makeRenderingUri(gadget).toString() : null;
-          return new MetadataResponse(context.getUrl().toString(), gadget.getSpec(),
-              iframeUrl, fields);
+          String iframeUrl =
+              fields.contains("iframeurl") ? iframeUriManager.makeRenderingUri(gadget).toString()
+                  : null;
+          MetadataResponseData response =
+              new MetadataResponseData(context.getUrl().toString(), gadget.getSpec(), iframeUrl);
+          return (GadgetsHandlerApi.MetadataResponse) beanFilter.createFilteredBean(beanDelegator
+              .createDelegator(response), fields);
         } catch (Exception e) {
           // Note: this error message is publicly visible in JSON-RPC response.
           throw new RpcException(context, FAILURE_METADATA, e);
@@ -178,13 +226,18 @@ public class GadgetsHandler {
   }
 
   // Hook to override in sub-class.
-  protected Callable<TokenResponse> createTokenJob(String url, BaseRequestItem request) {
+  protected Callable<GadgetsHandlerApi.TokenResponse> createTokenJob(String url,
+      BaseRequestItem request) {
     final TokenGadgetContext context = new TokenGadgetContext(url, request);
-    return new Callable<TokenResponse>() {
-      public TokenResponse call() throws Exception {
+    final Set<String> fields =
+        beanFilter.processBeanFields(request.getFields(DEFAULT_METADATA_FIELDS));
+    return new Callable<GadgetsHandlerApi.TokenResponse>() {
+      public GadgetsHandlerApi.TokenResponse call() throws Exception {
         try {
           String token = securityTokenCodec.encodeToken(context.getToken());
-          return new TokenResponse(context.getUrl().toString(), token);
+          TokenResponseData response = new TokenResponseData(context.getUrl().toString(), token);
+          return (GadgetsHandlerApi.TokenResponse) beanFilter.createFilteredBean(beanDelegator
+              .createDelegator(response), fields);
         } catch (Exception e) {
           // Note: this error message is publicly visible in JSON-RPC response.
           throw new RpcException(context, FAILURE_TOKEN, e);
@@ -194,8 +247,8 @@ public class GadgetsHandler {
   }
 
   /**
-   * Gadget context classes used to translate JSON BaseRequestItem into a
-   * more meaningful model objects that Java can work with.
+   * Gadget context classes used to translate JSON BaseRequestItem into a more
+   * meaningful model objects that Java can work with.
    */
 
   private abstract class AbstractGadgetContext extends GadgetContext {
@@ -234,9 +287,9 @@ public class GadgetsHandler {
       super(url, request);
       String lang = request.getParameter("language");
       String country = request.getParameter("country");
-      this.locale = (lang != null && country != null) ? new Locale(lang,country) :
-                    (lang != null) ? new Locale(lang) :
-                    GadgetSpec.DEFAULT_LOCALE;
+      this.locale =
+          (lang != null && country != null) ? new Locale(lang, country) : (lang != null)
+              ? new Locale(lang) : GadgetSpec.DEFAULT_LOCALE;
       this.ignoreCache = Boolean.valueOf(request.getParameter("ignoreCache"));
       this.debug = Boolean.valueOf(request.getParameter("debug"));
     }
@@ -288,18 +341,18 @@ public class GadgetsHandler {
    * container JS. They must be public for reflection to work.
    */
 
-  public static class BaseResponse {
+  public static class BaseResponseData {
     private final String url;
     private final String error;
 
     // Call this to indicate an error.
-    public BaseResponse(String url, String error) {
+    public BaseResponseData(String url, String error) {
       this.url = url;
       this.error = error;
     }
 
     // Have sub-class call this to indicate a success response.
-    protected BaseResponse(String url) {
+    protected BaseResponseData(String url) {
       this(url, null);
     }
 
@@ -312,118 +365,42 @@ public class GadgetsHandler {
     }
   }
 
-  public static class MetadataResponse extends BaseResponse {
+  public static class MetadataResponseData extends BaseResponseData {
     private final GadgetSpec spec;
     private final String iframeUrl;
-    private final Map<String, ViewResponse> views;
-    private final Set<String> fields;
 
-    public MetadataResponse(String url, GadgetSpec spec, String iframeUrl, Set<String> fields) {
+    public MetadataResponseData(String url, GadgetSpec spec, String iframeUrl) {
       super(url);
       this.spec = spec;
       this.iframeUrl = iframeUrl;
-      this.fields = fields;
-
-      // Do we need view data?
-      boolean viewsRequested = fields.contains("views");
-      for (String f: fields) {
-        if (f.startsWith("views")) {
-          viewsRequested = true;
-        }
-      }
-      if (viewsRequested) {
-        ImmutableMap.Builder<String, ViewResponse> builder = ImmutableMap.builder();
-        for (Map.Entry<String,View> entry : spec.getViews().entrySet()) {
-          builder.put(entry.getKey(), new ViewResponse(entry.getValue(), fields));
-        }
-        views = builder.build();
-      } else {
-        views = null;
-      }
     }
 
     public String getIframeUrl() {
-      return fields.contains("iframeUrl") ? iframeUrl : null;
+      return iframeUrl;
     }
 
     public String getChecksum() {
-      return fields.contains("checksum") ? spec.getChecksum() : null;
+      return spec.getChecksum();
     }
 
     public ModulePrefs getModulePrefs() {
-      return fields.contains("modulePrefs") ? spec.getModulePrefs() : null;
+      return spec.getModulePrefs();
     }
 
     public Map<String, UserPref> getUserPrefs() {
-      return fields.contains("userPrefs") ? spec.getUserPrefs() : null;
+      return spec.getUserPrefs();
     }
 
-    public Map<String, ViewResponse> getViews() {
-      return views;
+    public Map<String, View> getViews() {
+      return spec.getViews();
     }
   }
 
-  public static class ViewResponse {
-    private final View view;
-    private final Set<String> fields;
-
-    /**
-     * Return the actual item if the requested fields contains "views" or param
-     * @param item any item
-     * @param param a field to test for
-     * @param <T> any type
-     * @return Returns item if fields contains "views" or param
-     */
-    private <T> T filter(T item, String param) {
-      return (fields.contains("views") || fields.contains(param)) ? item : null;
-    }
-
-    public ViewResponse(View view, Set<String> fields) {
-      this.view = view;
-      this.fields = fields;
-    }
-
-    public String getName() {
-      return filter(view.getName(), "views.name");
-    }
-
-    public View.ContentType getType() {
-      return filter(view.getType(), "views.type");
-    }
-
-    public Uri getHref() {
-      return filter(view.getHref(), "views.href");
-    }
-
-    public Boolean getQuirks() {
-      return filter(view.getQuirks(), "views.quirks");
-    }
-
-    public String getContent() {
-      return fields.contains("views.content") ? view.getContent() : null;
-    }
-
-    public Integer getPreferredHeight() {
-      return filter(view.getPreferredHeight(), "views.preferredHeight");
-    }
-
-    public Integer getPreferredWidth() {
-      return filter(view.getPreferredWidth(), "views.preferredWidth");
-    }
-
-    public Boolean needsUserPrefSubstitution() {
-      return filter(view.needsUserPrefSubstitution(), "views.needsUserPrefSubstitution");
-    }
-
-    public Map<String, String> getAttributes() {
-      return filter(view.getAttributes(), "views.attributes");
-    }
-  }
 
-  public static class TokenResponse extends BaseResponse {
+  public static class TokenResponseData extends BaseResponseData {
     private final String token;
 
-    public TokenResponse(String url, String token) {
+    public TokenResponseData(String url, String token) {
       super(url);
       this.token = token;
     }
@@ -433,4 +410,4 @@ public class GadgetsHandler {
     }
   }
 
-}
\ No newline at end of file
+}

Added: shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/GadgetsHandlerApi.java
URL: http://svn.apache.org/viewvc/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/GadgetsHandlerApi.java?rev=982106&view=auto
==============================================================================
--- shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/GadgetsHandlerApi.java (added)
+++ shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/GadgetsHandlerApi.java Wed Aug  4 02:41:30 2010
@@ -0,0 +1,136 @@
+/*
+ * 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.gadgets.servlet;
+
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.protocol.conversion.BeanFilter.Required;
+// Keep imports clean, so it is clear what is used by API
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Gadget Handler Interface data.
+ * Classes in here specify the API data.
+ * Please do not reference run time classes, instead create new interface (keep imports clean!).
+ * Please avoid changes if possible, you might break external system that depend on the API.
+ */
+public class GadgetsHandlerApi {
+
+  public interface BaseResponse {
+    @Required
+    public String getUrl();
+    @Required
+    public String getError();
+  }
+
+  public interface MetadataResponse extends BaseResponse {
+    public String getIframeUrl();
+    public String getChecksum();
+    public ModulePrefs getModulePrefs();
+    public Map<String, UserPref> getUserPrefs();
+    public Map<String, View> getViews();
+  }
+
+  public enum ViewContentType {
+    HTML("html"), URL("url"), HTML_SANITIZED("x-html-sanitized");
+
+    private final String name;
+    private ViewContentType(String name) {
+      this.name = name;
+    }
+    @Override
+    public String toString() {
+      return name;
+    }
+  }
+
+  public interface View {
+    public String getName();
+    public ViewContentType getType();
+    public Uri getHref();
+    public boolean getQuirks();
+    public String getContent();
+    public int getPreferredHeight();
+    public int getPreferredWidth();
+    public boolean needsUserPrefSubstitution();
+    public Map<String, String> getAttributes();
+  }
+
+  public enum UserPrefDataType {
+    STRING, HIDDEN, BOOL, ENUM, LIST, NUMBER;
+  }
+
+  public interface UserPref {
+    public String getName();
+    public String getDisplayName();
+    public String getDefaultValue();
+    public boolean getRequired();
+    public UserPrefDataType getDataType();
+    public Map<String, String> getEnumValues();
+    public List<EnumValuePair> getOrderedEnumValues();
+  }
+
+  public interface EnumValuePair {
+    public String getValue();
+    public String getDisplayValue();
+  }
+
+  public interface ModulePrefs {
+    public String getTitle();
+    public Uri getTitleUrl();
+    public String getDescription();
+    public String getAuthor();
+    public String getAuthorEmail();
+    public Uri getScreenshot();
+    public Uri getThumbnail();
+    public String getDirectoryTitle();
+    public String getAuthorAffiliation();
+    public String getAuthorLocation();
+    public Uri getAuthorPhoto();
+    public String getAuthorAboutme();
+    public String getAuthorQuote();
+    public Uri getAuthorLink();
+    public boolean getScaling();
+    public boolean getScrolling();
+    public int getWidth();
+    public int getHeight();
+    public List<String> getCategories();
+    public Map<String, Feature> getFeatures();
+    public Map<String, LinkSpec> getLinks();
+    // TODO: Provide better interface for locale if needed
+    // public Map<Locale, LocaleSpec> getLocales();
+  }
+
+  public interface Feature {
+    public String getName();
+    public boolean getRequired();
+    // TODO: Handle multi map if params are needed
+    // public Multimap<String, String> getParams();
+  }
+
+  public interface LinkSpec {
+    public String getRel();
+    public Uri getHref();
+  }
+
+  public interface TokenResponse extends BaseResponse {
+    public String getToken();
+  }
+}

Modified: shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/GadgetsHandlerTest.java
URL: http://svn.apache.org/viewvc/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/GadgetsHandlerTest.java?rev=982106&r1=982105&r2=982106&view=diff
==============================================================================
--- shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/GadgetsHandlerTest.java (original)
+++ shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/GadgetsHandlerTest.java Wed Aug  4 02:41:30 2010
@@ -33,6 +33,8 @@ import org.apache.shindig.protocol.Defau
 import org.apache.shindig.protocol.HandlerExecutionListener;
 import org.apache.shindig.protocol.HandlerRegistry;
 import org.apache.shindig.protocol.RpcHandler;
+import org.apache.shindig.protocol.conversion.BeanDelegator;
+import org.apache.shindig.protocol.conversion.BeanFilter;
 import org.apache.shindig.protocol.conversion.BeanJsonConverter;
 import org.apache.shindig.protocol.multipart.FormDataItem;
 import org.easymock.EasyMock;
@@ -73,7 +75,8 @@ public class GadgetsHandlerTest extends 
 
   private void registerGadgetsHandler(SecurityTokenCodec codec) {
     GadgetsHandler handler =
-        new GadgetsHandler(new TestExecutorService(), processor, urlGenerator, codec);
+        new GadgetsHandler(new TestExecutorService(), processor, urlGenerator, codec,
+          new BeanFilter());
     registry = new DefaultHandlerRegistry(
         injector, converter, new HandlerExecutionListener.NoOpHandler());
     registry.addHandlers(ImmutableSet.<Object> of(handler));
@@ -272,4 +275,16 @@ public class GadgetsHandlerTest extends 
     assertNotNull("got gadget2", gadget2);
     assertEquals(GadgetsHandler.FAILURE_METADATA, gadget2.getString("error"));
   }
+
+  // Next test verify that the API data classes are configured correctly.
+  // The mapping is done using reflection in runtime, so this test verify mapping is complete
+  // this test will prevent from not intended change to the API.
+  // DO NOT REMOVE TEST
+  @Test
+  public void testHandlerDataDelegation() throws Exception {
+    BeanDelegator delegator = new BeanDelegator(
+        GadgetsHandler.apiClasses, GadgetsHandler.enumConversionMap);
+    delegator.validate();
+  }
+
 }