/* * Copyright (c) 2015 Spotify AB. * * 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 com.spotify.heroic.test; import com.fasterxml.jackson.annotation.JsonCreator; import com.google.common.base.CaseFormat; import com.google.common.base.Converter; import com.google.common.base.Throwables; import lombok.RequiredArgsConstructor; import java.beans.ConstructorProperties; import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.lang.reflect.Parameter; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Optional; import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotSame; import static org.junit.Assert.assertTrue; public class LombokDataTest { private static final Converter<String, String> LOWER_TO_UPPER = CaseFormat.LOWER_CAMEL.converterTo(CaseFormat.UPPER_CAMEL); public static void verifyClass(final Class<?> cls) { verifyClassBuilder(cls).verify(); } @lombok.Data static class FieldSpec { private final Class<?> rawType; private final Object primary; private final Object secondary; private final String name; private final Optional<Method> getter; } public static Builder verifyClassBuilder(final Class<?> cls) { return new Builder(cls); } @RequiredArgsConstructor public static class Builder { private final Class<?> cls; /** * Check that there are no conflicting value builders. */ private boolean checkJson = true; /** * Custom value suppliers. */ private final List<ValueSuppliers.ValueSupplier> valueSuppliers = new ArrayList<>(); /** * Getters to ignore. */ private final Set<String> ignoreGetters = new HashSet<>(); /** * Ignore checking getters. */ private boolean checkGetters = true; public Builder checkJson(final boolean checkJson) { this.checkJson = checkJson; return this; } public Builder valueSupplier(ValueSuppliers.ValueSupplier valueSupplier) { this.valueSuppliers.add(valueSupplier); return this; } public Builder ignoreGetter(final String name) { this.ignoreGetters.add(name); return this; } public Builder checkGetters(final boolean checkGetters) { this.checkGetters = checkGetters; return this; } public void verify() { final ValueSuppliers suppliers = new ValueSuppliers(valueSuppliers); final FakeValueProvider valueProvider = new FakeValueProvider(suppliers); final Constructor<?> constructor = Arrays .stream(cls.getConstructors()) .filter(c -> c.isAnnotationPresent(ConstructorProperties.class)) .findAny() .orElseThrow(() -> new IllegalArgumentException( "No constructor annotated with @ConstructorProperties")); final List<FieldSpec> specs = buildFieldSpecs(constructor, valueProvider); if (checkGetters) { verifyGetters(constructor, specs); } verifyEquals(constructor, specs); verifyToString(constructor, specs); if (checkJson) { verifyJson(cls); } } private void verifyJson(final Class<?> cls) { final List<Constructor<?>> constructors = Arrays .stream(cls.getConstructors()) .filter(c -> c.isAnnotationPresent(ConstructorProperties.class) || c.isAnnotationPresent(JsonCreator.class)) .collect(Collectors.toList()); final List<Method> jsonCreators = Arrays .stream(cls.getMethods()) .filter(m -> ((m.getModifiers() & Modifier.STATIC) != 0) && m.isAnnotationPresent(JsonCreator.class)) .collect(Collectors.toList()); if (constructors.size() + jsonCreators.size() != 1) { throw new AssertionError( "Constructors (" + constructors + ") and @JsonCreator methods (" + jsonCreators + ") conflicting for JSON de-serialization"); } } private void verifyGetters( final Constructor<?> constructor, final List<FieldSpec> specs ) { final Object instance = newInstance(constructor, specs, false); /* test getters */ for (final FieldSpec f : specs) { f.getGetter().ifPresent(getter -> { final Object value; try { value = getter.invoke(instance); } catch (final ReflectiveOperationException e) { throw Throwables.propagate(e); } assertEquals("Expected value from getter (" + f.getName() + ") is same", f.getPrimary(), value); }); } } private void verifyToString( final Constructor<?> constructor, final List<FieldSpec> specs ) { final Object instance = newInstance(constructor, specs, false); final Class<?> cls = constructor.getDeclaringClass(); final Method toString; try { toString = cls.getMethod("toString"); } catch (final NoSuchMethodException e) { throw Throwables.propagate(e); } /* test toString */ try { final String toStringResult = (String) toString.invoke(instance); assertNotNull(toStringResult); assertTrue(toStringResult.contains(cls.getSimpleName())); } catch (final ReflectiveOperationException e) { throw Throwables.propagate(e); } } private void verifyEquals( final Constructor<?> constructor, final List<FieldSpec> specs ) { final Object current = newInstance(constructor, specs, false); final Object other = newInstance(constructor, specs, false); final Object different = newInstance(constructor, specs, true); assertNotSame(current, other); final Method equals; try { equals = constructor.getDeclaringClass().getMethod("equals", Object.class); } catch (final NoSuchMethodException e) { throw Throwables.propagate(e); } assertTrue(invoke(equals, current, current)); assertTrue(invoke(equals, current, other)); assertFalse(invoke(equals, current, different)); } @SuppressWarnings("unchecked") private <T> T invoke( final Method equals, final Object instance, final Object... arguments ) { try { return (T) equals.invoke(instance, arguments); } catch (final ReflectiveOperationException e) { throw Throwables.propagate(e); } } private Object newInstance( final Constructor<?> constructor, final List<FieldSpec> specs, final boolean secondary ) { final Function<FieldSpec, Object> accessor = secondary ? FieldSpec::getSecondary : FieldSpec::getPrimary; final Object[] arguments = specs.stream().map(accessor).toArray(Object[]::new); try { return constructor.newInstance(arguments); } catch (ReflectiveOperationException e) { throw Throwables.propagate(e); } } private List<FieldSpec> buildFieldSpecs( final Constructor<?> constructor, final FakeValueProvider valueProvider ) { final Parameter[] parameters = constructor.getParameters(); final Type[] types = Arrays.stream(parameters).map(Parameter::getParameterizedType).toArray(Type[]::new); final String[] names = constructor.getAnnotation(ConstructorProperties.class).value(); if (types.length != names.length) { throw new IllegalArgumentException( "@ConstructorProperties length does not match number of parameters"); } final List<FieldSpec> specs = new ArrayList<>(); for (int i = 0; i < types.length; i++) { final Type type = types[i]; final Class<?> rawType = toRawType(type); final String name = names[i]; final Optional<Method> getter; if (!checkGetters || ignoreGetters.contains(name)) { getter = Optional.empty(); } else { if (rawType.equals(boolean.class)) { getter = Optional.of(getGetterMethod(cls, "is" + LOWER_TO_UPPER.convert(name))); } else { getter = Optional.of(getGetterMethod(cls, "get" + LOWER_TO_UPPER.convert(name))); } } final Object primary = valueProvider.lookup(type, false, name); final Object secondary = valueProvider.lookup(type, true, name); specs.add(new FieldSpec(rawType, primary, secondary, name, getter)); } return specs; } private Class<?> toRawType(final Type type) { if (type instanceof Class) { return (Class<?>) type; } if (type instanceof ParameterizedType) { return toRawType(((ParameterizedType) type).getRawType()); } throw new IllegalArgumentException("Cannot get raw type for (" + type + ")"); } private Method getGetterMethod(final Class<?> cls, final String name) { try { return cls.getMethod(name); } catch (NoSuchMethodException e) { throw Throwables.propagate(e); } } } }