All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.faktorips.util.MethodAccess Maven / Gradle / Ivy

The newest version!
/*******************************************************************************
 * Copyright (c) Faktor Zehn GmbH - faktorzehn.org
 * 
 * This source code is available under the terms of the AGPL Affero General Public License version
 * 3.
 * 
 * Please see LICENSE.txt for full license terms, including the additional permissions and
 * restrictions as well as the possibility of alternative license terms.
 *******************************************************************************/
package org.faktorips.util;

import static java.util.Objects.requireNonNull;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.text.MessageFormat;
import java.util.Arrays;
import java.util.Optional;
import java.util.stream.Collectors;

import org.apache.commons.lang3.ClassUtils;
import org.faktorips.runtime.Message;
import org.faktorips.runtime.MessageList;
import org.faktorips.runtime.internal.IpsStringUtils;

/**
 * Wraps access to a method. Allows {@link #check(MessageList, String) checks} on the method and its
 * {@link #invoke(String, Object, Object...) invocation}.
 */
public class MethodAccess {

    private final Class clazz;
    private final String methodName;
    private final Class[] parameterTypes;

    private Optional method;
    private RuntimeException exception;

    private MethodAccess(Class clazz, String methodName, Class... parameterTypes) {
        this.clazz = clazz;
        this.methodName = methodName;
        this.parameterTypes = parameterTypes;
        try {
            method = Optional.of(getMethod());
            // CSOFF: Illegal Catch
        } catch (RuntimeException e) {
            exception = e;
            method = Optional.empty();
        }
    }

    /**
     * Creates a {@link MethodAccess} for the given class and method name.
     * 

* If the method can not be found, {@link #check(MessageList, String) checks} and * {@link #invoke(String, Object, Object...) invocations} will fail when called. * * @param clazz a class * @param methodName the name of a method of the given class * * @throws NullPointerException if no class or method name is given * @throws IllegalArgumentException if the method name is empty */ public static final MethodAccess of(Class clazz, String methodName, Class... parameterTypes) { requireNonNull(clazz, "clazz must not be null"); if (IpsStringUtils.isBlank(requireNonNull(methodName, "methodName must not be null"))) { throw new IllegalArgumentException("methodName must not be empty"); } for (Class parameterType : parameterTypes) { requireNonNull(parameterType, "parameterType must not be null"); } return new MethodAccess(clazz, methodName, parameterTypes); } private Method getMethod() { return Arrays.stream(clazz.getMethods()) .filter(m -> m.getName().equals(methodName)) .filter(m -> m.getParameterCount() == parameterTypes.length) .filter(m -> { var actualParameterTypes = m.getParameterTypes(); for (int i = 0; i < parameterTypes.length; i++) { if (!parameterTypes[i].isAssignableFrom(actualParameterTypes[i])) { return false; } } return true; }) .findFirst() .orElseThrow(() -> new IllegalStateException( "Unable to find the method " + methodName + "(" + Arrays.stream(parameterTypes).map(Object::toString).collect(Collectors.joining()) + ") on the adapted class " + clazz)); } /** * Invokes the wrapped method on the given object with the given arguments. * * @param methodDescription a description of what the method does, like "to find a value by * name", that will be included in an exception's message if one is thrown * @param object an instance of the wrapped class on which the method should be invoked * @param args the arguments (if any) to be passed to the method * @param the expected type of the return value * @return the method's return value * * @throws MethodAccessException if invocation of the method fails */ @SuppressWarnings("unchecked") public T invoke(String methodDescription, Object object, Object... args) { try { return (T)method.orElseThrow(() -> exception).invoke(object, args); } catch (IllegalArgumentException | InvocationTargetException | IllegalAccessException | IllegalStateException | NullPointerException e) { throw new MethodAccessException( "Unable to invoke the method " + methodDescription + " " + methodName + " on the class: " + clazz, e); } } /** * Invokes the wrapped static method on the wrapped class with the given arguments. * * @param methodDescription a description of what the method does, like "to find a value by * name", that will be included in an exception's message if one is thrown * @param args the arguments (if any) to be passed to the method * @param the expected type of the return value * @return the method's return value * * @throws MethodAccessException if invocation of the method fails */ public T invokeStatic(String methodDescription, Object... args) { return invoke(methodDescription, null, args); } /** * Returns whether the wrapped method actually exists. */ public boolean exists() { return method.isPresent(); } @Override public String toString() { return "MethodAccess [" + clazz + "." + methodName + (parameterTypes != null ? "(" + Arrays.toString(parameterTypes) + ")" : "()"); } /** * Creates a new {@link Check} object for this class and method name. *

* All checks called on the {@link Check} object report problems to the given * {@link MessageList} using the given prefix for their message codes. *

* The first check to be called should be {@link Check#exists()}, because other checks won't * work if there is no method to check. * * @param messageList a list of messages to which this check can report problems * @param msgCodePrefix a prefix to be used for message codes */ public Check check(MessageList messageList, String msgCodePrefix) { requireNonNull(messageList, "messageList must not be null"); requireNonNull(msgCodePrefix, "msgCodePrefix must not be null"); return new Check(messageList, msgCodePrefix); } /** * Offers multiple checks on a {@link MethodAccess} in a fluent API. */ public class Check { public static final String MSG_CODE_SUFFIX_DOES_NOT_EXIST = "_NOT_FOUND"; public static final String MSG_CODE_SUFFIX_NOT_STATIC = "_NOT_STATIC"; public static final String MSG_CODE_SUFFIX_STATIC = "_STATIC"; public static final String MSG_CODE_SUFFIX_INCOMPATIBLE_RETURN_TYPE = "_INCOMPATIBLE_RETURN_TYPE"; private final MessageList messageList; private final String msgCodePrefix; /** * Creates a new {@link Check} object. All checks called on this object report problems to * the given {@link MessageList} using the given prefix for their message codes. *

* The first check to be called should be {@link #exists()}, because other checks won't work * if there is no method to check. * * @param messageList a list of messages to which this check can report problems * @param msgCodePrefix a prefix to be used for message codes */ private Check(MessageList messageList, String msgCodePrefix) { this.messageList = messageList; this.msgCodePrefix = msgCodePrefix; } /** * Checks whether the method exists. If the method does not exist or cannot be accessed from * this class, an error message with a message code ending in * {@value #MSG_CODE_SUFFIX_DOES_NOT_EXIST} will be added to the message list. * * @return this {@link Check} object for further checks. */ public Check exists() { if (!MethodAccess.this.exists()) { String text = MessageFormat.format("The Java class {0} hasn''t got a method {1}", clazz, methodName); if (messageList.getMessagesFor(clazz, methodName) .getMessagesByCode(msgCodePrefix + MSG_CODE_SUFFIX_DOES_NOT_EXIST).isEmpty()) { messageList.add(Message .error(text) .code(msgCodePrefix + MSG_CODE_SUFFIX_DOES_NOT_EXIST) .invalidObjectWithProperties(clazz, methodName).create()); } } return this; } /** * If the method {@link #exists()}, checks whether the method is static. If the method is * not static, an error message with a message code ending in * {@value #MSG_CODE_SUFFIX_NOT_STATIC} will be added to the message list. * * @return this {@link Check} object for further checks. */ public Check isStatic() { method.ifPresentOrElse(m -> { if (!Modifier.isStatic(m.getModifiers())) { messageList.add(Message .error("The method " + m + " is not static.") .code(msgCodePrefix + MSG_CODE_SUFFIX_NOT_STATIC) .invalidObjectWithProperties(clazz, methodName).create()); } }, this::exists); return this; } /** * If the method {@link #exists()}, checks whether the method is not static. If the method * is static, an error message with a message code ending in * {@value #MSG_CODE_SUFFIX_STATIC} will be added to the message list. * * @return this {@link Check} object for further checks. */ public Check isNotStatic() { method.ifPresentOrElse(m -> { if (Modifier.isStatic(m.getModifiers())) { messageList.add(Message .error("The method " + m + " is static.") .code(msgCodePrefix + MSG_CODE_SUFFIX_STATIC) .invalidObjectWithProperties(clazz, methodName).create()); } }, this::exists); return this; } /** * If the method {@link #exists()}, checks whether the method return type is compatible to * any one of the given return types. If the method returns something different, an error * message with a message code ending in {@value #MSG_CODE_SUFFIX_INCOMPATIBLE_RETURN_TYPE} * will be added to the message list. *

* Compatible primitive datatypes and their wrappers will be mapped automatically, so for * example {@link Boolean Boolean.class} and {@link Boolean#TYPE} do not need to be both * specified. * * @return this {@link Check} object for further checks. */ public Check returnTypeIsCompatible(Class... expectedReturnTypes) { method.ifPresent(m -> { if (!isCompatible(m.getReturnType(), expectedReturnTypes)) { messageList.add(Message .error("The method " + m + " does not return a " + Arrays.stream(expectedReturnTypes).map(Class::getSimpleName) .collect(Collectors.joining(" or "))) .code(msgCodePrefix + MSG_CODE_SUFFIX_INCOMPATIBLE_RETURN_TYPE) .invalidObjectWithProperties(clazz, methodName).create()); } }); return this; } private boolean isCompatible(Class actualType, Class... expectedTypes) { Class wrappedActualType = ClassUtils.primitiveToWrapper(actualType); return Arrays.stream(expectedTypes) .map(ClassUtils::primitiveToWrapper) .anyMatch(cl -> ((Class)cl).isAssignableFrom(wrappedActualType)); } } public static class MethodAccessException extends RuntimeException { private static final long serialVersionUID = 1L; public MethodAccessException(String message) { super(message); } public MethodAccessException(String message, Throwable cause) { super(message, cause); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy