com.spotify.hamcrest.pojo.IsPojo.java Source code

Java tutorial

Introduction

Here is the source code for com.spotify.hamcrest.pojo.IsPojo.java

Source

/*-
 * -\-\-
 * hamcrest-pojo
 * --
 * Copyright (C) 2017 Spotify AB
 * --
 * Licensed 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.hamcrest.pojo;

import static java.util.Arrays.stream;
import static java.util.Objects.requireNonNull;

import com.google.auto.value.AutoValue;
import com.google.common.base.CaseFormat;
import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableMap;
import com.spotify.hamcrest.util.DescriptionUtils;
import java.lang.invoke.SerializedLambda;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.security.AccessController;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional;
import java.util.function.Consumer;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.hamcrest.StringDescription;
import org.hamcrest.TypeSafeDiagnosingMatcher;

@AutoValue
public abstract class IsPojo<A> extends TypeSafeDiagnosingMatcher<A> {

    IsPojo() {
        // Prevent outside instantiation.
    }

    abstract Class<A> cls();

    abstract ImmutableMap<String, MethodHandler<A, ?>> methodHandlers();

    public static <A> IsPojo<A> pojo(final Class<A> cls) {
        return builder(cls).build();
    }

    public <T> IsPojo<A> where(final String methodName, final Matcher<T> returnValueMatcher) {
        return where(methodName, self -> {
            final Method method = methodWithName(methodName, self);
            method.setAccessible(true);
            @SuppressWarnings("unchecked")
            final T returnValue = (T) method.invoke(self);
            return returnValue;
        }, returnValueMatcher);
    }

    public <T> IsPojo<A> where(final MethodReference<A, T> methodReference, final Matcher<T> returnValueMatcher) {
        final SerializedLambda serializedLambda = serializeLambda(methodReference);

        ensureDirectMethodReference(serializedLambda);

        return where(serializedLambda.getImplMethodName(), methodReference, returnValueMatcher);
    }

    private <T> IsPojo<A> where(final String methodName, final MethodReference<A, T> valueExtractor,
            final Matcher<T> matcher) {

        return toBuilder().methodHandler(methodName, MethodHandler.create(valueExtractor, matcher)).build();
    }

    private Method methodWithName(String methodName, A self) throws NoSuchMethodException {
        try {
            return self.getClass().getDeclaredMethod(methodName);
        } catch (NoSuchMethodException e) {
            return self.getClass().getMethod(methodName);
        }
    }

    public IsPojo<A> withProperty(String property, Matcher<?> valueMatcher) {
        return where("get" + CaseFormat.LOWER_CAMEL.to(CaseFormat.UPPER_CAMEL, property), valueMatcher);
    }

    private static <A> Builder<A> builder(final Class<A> cls) {
        return new AutoValue_IsPojo.Builder<A>().cls(cls);
    }

    abstract Builder<A> toBuilder();

    @AutoValue.Builder
    abstract static class Builder<A> {

        abstract Builder<A> cls(final Class<A> cls);

        abstract ImmutableMap.Builder<String, MethodHandler<A, ?>> methodHandlersBuilder();

        Builder<A> methodHandler(final String methodName, final MethodHandler<A, ?> handler) {
            methodHandlersBuilder().put(methodName, handler);
            return this;
        }

        abstract IsPojo<A> build();
    }

    @Override
    protected boolean matchesSafely(A item, Description mismatchDescription) {
        if (!cls().isInstance(item)) {
            mismatchDescription.appendText("not an instance of " + cls().getName());
            return false;
        }

        final Map<String, Consumer<Description>> mismatches = new LinkedHashMap<>();

        methodHandlers().forEach((methodName, handler) -> matchMethod(item, handler)
                .ifPresent(descriptionConsumer -> mismatches.put(methodName, descriptionConsumer)));

        if (!mismatches.isEmpty()) {
            mismatchDescription.appendText(cls().getSimpleName()).appendText(" ");
            DescriptionUtils.describeNestedMismatches(methodHandlers().keySet(), mismatchDescription, mismatches,
                    IsPojo::describeMethod);
            return false;
        }

        return true;
    }

    @Override
    public void describeTo(Description description) {
        description.appendText(cls().getSimpleName()).appendText(" {\n");

        methodHandlers().forEach((methodName, handler) -> {
            final Matcher<?> matcher = handler.matcher();

            description.appendText("  ").appendText(methodName).appendText("(): ");

            Description innerDescription = new StringDescription();
            matcher.describeTo(innerDescription);

            indentDescription(description, innerDescription);
        });
        description.appendText("}");
    }

    private static <A> Optional<Consumer<Description>> matchMethod(final A item,
            final MethodHandler<A, ?> handler) {
        final Matcher<?> matcher = handler.matcher();
        final MethodReference<A, ?> reference = handler.reference();

        try {
            final Object value = reference.apply(item);
            if (!matcher.matches(value)) {
                return Optional.of(d -> matcher.describeMismatch(value, d));
            } else {
                return Optional.empty();
            }
        } catch (IllegalAccessException e) {
            return Optional.of(d -> d.appendText("not accessible"));
        } catch (NoSuchMethodException e) {
            return Optional.of(d -> d.appendText("did not exist"));
        } catch (InvocationTargetException e) {
            final Throwable cause = e.getCause();
            return Optional
                    .of(d -> d.appendText("threw an exception: ").appendText(cause.getClass().getCanonicalName())
                            .appendText(": ").appendText(cause.getMessage()));
        } catch (Exception e) {
            return Optional.of(d -> d.appendText("threw an exception: ").appendText(e.getClass().getCanonicalName())
                    .appendText(": ").appendText(e.getMessage()));
        }
    }

    private static void describeMethod(String name, Description description) {
        description.appendText(name).appendText("()");
    }

    private void indentDescription(Description description, Description innerDescription) {
        description.appendText(Joiner.on("\n  ").join(Splitter.on('\n').split(innerDescription.toString())))
                .appendText("\n");
    }

    /**
     * Method uses serialization trick to extract information about lambda,
     * to give understandable name in case of mismatch.
     *
     * @param lambda lambda to extract the name from
     * @return a serialized version of the lambda, containing useful information for introspection
     */
    private static SerializedLambda serializeLambda(final Object lambda) {
        requireNonNull(lambda);

        final Method writeReplace;
        try {
            writeReplace = AccessController.doPrivileged((PrivilegedExceptionAction<Method>) () -> {
                Method method = lambda.getClass().getDeclaredMethod("writeReplace");
                method.setAccessible(true);
                return method;
            });
        } catch (PrivilegedActionException e) {
            throw new IllegalStateException("Cannot serialize lambdas in unprivileged context", e);
        }

        try {
            return (SerializedLambda) writeReplace.invoke(lambda);
        } catch (ClassCastException | IllegalAccessException | InvocationTargetException e) {
            throw new IllegalArgumentException("Could not serialize as a lambda (is it a lambda?): " + lambda, e);
        }
    }

    private static void ensureDirectMethodReference(final SerializedLambda serializedLambda) {
        try {
            final Class<?> implClass = Class.forName(serializedLambda.getImplClass().replace('/', '.'));
            if (stream(implClass.getMethods())
                    .noneMatch(m -> m.getName().equals(serializedLambda.getImplMethodName()) && !m.isSynthetic())) {
                throw new IllegalArgumentException("The supplied lambda is not a direct method reference");
            }
        } catch (final ClassNotFoundException e) {
            throw new IllegalStateException(
                    "serializeLambda returned a SerializedLambda pointing to an invalid class", e);
        }
    }

    @AutoValue
    abstract static class MethodHandler<A, T> {

        abstract MethodReference<A, T> reference();

        abstract Matcher<T> matcher();

        static <A, T> MethodHandler<A, T> create(final MethodReference<A, T> reference, final Matcher<T> matcher) {
            return new AutoValue_IsPojo_MethodHandler<>(reference, matcher);
        }
    }
}