001/**
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018
019package org.apache.hadoop.hbase.util;
020
021import static org.junit.Assert.assertTrue;
022
023import java.lang.reflect.Method;
024import java.lang.reflect.Modifier;
025import java.util.HashMap;
026import java.util.HashSet;
027import java.util.Map;
028import java.util.Set;
029
030/**
031 * Utility class to check whether a given class conforms to builder-style:
032 * Foo foo =
033 *   new Foo()
034 *     .setBar(bar)
035 *     .setBaz(baz)
036 */
037public class BuilderStyleTest {
038
039  /*
040   * If a base class Foo declares a method setFoo() returning Foo, then the subclass should
041   * re-declare the methods overriding the return class with the subclass:
042   *
043   * class Foo {
044   *   Foo setFoo() {
045   *     ..
046   *     return this;
047   *   }
048   * }
049   *
050   * class Bar {
051   *   Bar setFoo() {
052   *     return (Bar) super.setFoo();
053   *   }
054   * }
055   *
056   */
057  @SuppressWarnings("rawtypes")
058  public static void assertClassesAreBuilderStyle(Class... classes) {
059    for (Class clazz : classes) {
060      System.out.println("Checking " + clazz);
061      Method[] methods = clazz.getDeclaredMethods();
062      Map<String, Set<Method>> methodsBySignature = new HashMap<>();
063      for (Method method : methods) {
064        if (!Modifier.isPublic(method.getModifiers())) {
065          continue; // only public classes
066        }
067        Class<?> ret = method.getReturnType();
068        if (method.getName().startsWith("set") || method.getName().startsWith("add")) {
069          System.out.println("  " + clazz.getSimpleName() + "." + method.getName() + "() : "
070            + ret.getSimpleName());
071
072          // because of subclass / super class method overrides, we group the methods fitting the
073          // same signatures because we get two method definitions from java reflection:
074          // Mutation.setDurability() : Mutation
075          //   Delete.setDurability() : Mutation
076          // Delete.setDurability() : Delete
077          String sig = method.getName();
078          for (Class<?> param : method.getParameterTypes()) {
079            sig += param.getName();
080          }
081          Set<Method> sigMethods = methodsBySignature.get(sig);
082          if (sigMethods == null) {
083            sigMethods = new HashSet<>();
084            methodsBySignature.put(sig, sigMethods);
085          }
086          sigMethods.add(method);
087        }
088      }
089      // now iterate over the methods by signatures
090      for (Map.Entry<String, Set<Method>> e : methodsBySignature.entrySet()) {
091        // at least one of method sigs should return the declaring class
092        boolean found = false;
093        for (Method m : e.getValue()) {
094          found = clazz.isAssignableFrom(m.getReturnType());
095          if (found) break;
096        }
097        String errorMsg = "All setXXX()|addXX() methods in " + clazz.getSimpleName()
098            + " should return a " + clazz.getSimpleName() + " object in builder style. "
099            + "Offending method:" + e.getValue().iterator().next().getName();
100        assertTrue(errorMsg, found);
101      }
102    }
103  }
104}