Java tutorial
/*- * -\-\- * 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); } } }