/* * Copyright 2014, Armenak Grigoryan, and individual contributors as indicated * by the @authors tag. See the copyright.txt in the distribution for a * full listing of individual contributors. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. */ package com.strider.datadefender.requirement.plan; import com.strider.datadefender.functions.NamedParameter; import com.strider.datadefender.requirement.TypeConverter; import com.strider.datadefender.requirement.registry.ClassAndFunctionRegistry; import com.strider.datadefender.requirement.registry.RequirementFunction; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.sql.SQLException; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.stream.Collectors; import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlAttribute; import javax.xml.bind.annotation.XmlElement; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.ClassUtils; import org.apache.commons.lang3.StringUtils; import lombok.AccessLevel; import lombok.Data; import lombok.Getter; import lombok.Setter; import lombok.extern.log4j.Log4j2; /** * * @author Zaahid Bateson */ @Log4j2 @Data @XmlAccessorType(XmlAccessType.NONE) public class Function implements Invokable { @Setter(AccessLevel.NONE) @XmlAttribute(name = "name") private String functionName; private Method function; @XmlAttribute(name = "combiner-glue") private String combinerGlue; private Object combinerGlueObject; @XmlElement(name = "argument") private List<Argument> arguments; private boolean isCombinerFunction = false; @Getter(AccessLevel.NONE) @Setter(AccessLevel.NONE) private ClassAndFunctionRegistry registry; public Function() { this(ClassAndFunctionRegistry.singleton()); } public Function(ClassAndFunctionRegistry registry) { this.registry = registry; } public Function(String functionName, boolean isCombinerFunction) { this(); this.functionName = functionName; this.isCombinerFunction = isCombinerFunction; } /** * Setter for 'Function' element. * * @param fn */ public void setFunction(Method fn) { function = fn; functionName = fn.getName(); } /** * Returns true if the underlying method is static */ public boolean isStatic() { return Modifier.isStatic(function.getModifiers()); } /** * Looks for a class/method in the passed Function parameter in the form * com.package.Class#methodName, com.package.Class.methodName, or * com.package.Class::methodName. * * @return * @throws ClassNotFoundException */ private List<Method> getFunctionCandidates(Class<?> returnType) throws ClassNotFoundException { int index = StringUtils.lastIndexOfAny(functionName, "#", ".", "::"); if (index == -1) { throw new IllegalArgumentException( "Function element is empty or incomplete: " + functionName ); } String cn = functionName.substring(0, index); String fn = StringUtils.stripStart(functionName.substring(index), "#.:"); int argCount = CollectionUtils.size(arguments); log.debug("Looking for function in class {} with name {} and {} parameters", cn, fn, argCount); Class<?> clazz = registry.getClassForName(cn); List<Method> methods = Arrays.asList(clazz.getMethods()); return methods .stream() .filter((m) -> { if (!StringUtils.equals(fn, m.getName()) || !Modifier.isPublic(m.getModifiers())) { return false; } final int ac = (!isCombinerFunction || Modifier.isStatic(m.getModifiers())) ? argCount : 1; log.debug( "Candidate function {} needs {} parameters and gives {} return type, " + "looking for {} arguments and {} return type", () -> m.getName(), () -> m.getParameterCount(), () -> m.getReturnType(), () -> ac, () -> returnType ); return (m.getParameterCount() == ac && TypeConverter.isConvertible(m.getReturnType(), returnType)); }) .collect(Collectors.toList()); } /** * * @param type */ private void initializeCombinerGlue(Class<?> type) throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException { if (!isCombinerFunction && combinerGlue != null) { log.debug("Converting combinerGlue {} to object of type {}", combinerGlue, type); combinerGlueObject = TypeConverter.convert(combinerGlue, type); } } /** * Finds a method in the passed candidates with parameters matching the * arguments assigned to the current object, and returns it, or null if not * found. * * @param candidates * @return */ private Method findCandidateFunction(List<Method> candidates) { final Map<String, Argument> mappedArgs = CollectionUtils.emptyIfNull(arguments).stream() .collect(Collectors.toMap(Argument::getName, (o) -> o, (x, y) -> y)); return candidates.stream().filter((m) -> { int index = -1; for (java.lang.reflect.Parameter p : m.getParameters()) { ++index; Argument arg = arguments.get(index); NamedParameter named = p.getAnnotation(NamedParameter.class); if (named != null && mappedArgs.containsKey(named.value())) { arg = mappedArgs.get(named.value()); } else if (mappedArgs.containsKey(p.getName())) { arg = mappedArgs.get(p.getName()); } if (arg == null || !TypeConverter.isConvertible(p.getType(), arg.getType())) { return false; } } return true; }).sorted((a, b) -> { int score = 0; java.lang.reflect.Parameter[] aps = a.getParameters(); java.lang.reflect.Parameter[] bps = b.getParameters(); log.debug("Sorting method: {}", a.getName()); for (int i = 0; i < aps.length; ++i) { Argument arg = arguments.get(i); NamedParameter named = aps[i].getAnnotation(NamedParameter.class); if (named != null && mappedArgs.containsKey(named.value())) { arg = mappedArgs.get(named.value()); } else if (mappedArgs.containsKey(aps[i].getName())) { arg = mappedArgs.get(aps[i].getName()); } int s = TypeConverter.compareConversion(arg.getType(), aps[i].getType(), bps[i].getType()); log.debug("Comparing arguments for sorting: {} with: {} and: {}, result: {}", arg.getType(), aps[i].getType(), bps[i].getType(), s); score += s; } log.debug("Score between {}, {}: {}", a, b, score); return score; }).findFirst().orElse(null); } /** * Specialized function finder for a combiner that looks for either a single * parameter for a non-static function that would be run on the first * argument, or a two-parameter static function with compatible types. * * @param candidates * @return */ private Method findCombinerCandidateFunction(List<Method> candidates) { return candidates.stream().filter((m) -> { boolean isStatic = Modifier.isStatic(m.getModifiers()); int count = m.getParameterCount(); if (count == 0 || (isStatic && count != 2) || (!isStatic && count != 1)) { return false; } int index = -1; if (!isStatic && !RequirementFunction.class.isAssignableFrom(m.getDeclaringClass())) { ++index; if (!TypeConverter.isConvertible(arguments.get(index).getType(), m.getDeclaringClass())) { return false; } } for (java.lang.reflect.Parameter p : m.getParameters()) { ++index; Argument arg = arguments.get(index); if (!TypeConverter.isConvertible(p.getType(), arg.getType())) { return false; } } return true; }).findFirst().orElse(null); } /** * Uses functionName and arguments to find the method to associate with * 'Function'. * * @param returnType */ Method initialize(Class<?> returnType) throws ClassNotFoundException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException { log.debug("Initializing function {}", functionName); initializeCombinerGlue(returnType); List<Method> candidates = getFunctionCandidates(returnType); log.debug( "Found method candidates: {}", () -> CollectionUtils.emptyIfNull(candidates).stream() .map((m) -> m.getName() + " " + m.getParameterCount()) .collect(Collectors.toList()) ); if (!isCombinerFunction) { function = findCandidateFunction(candidates); } else { function = findCombinerCandidateFunction(candidates); } log.debug("Function references method: {}", () -> (function == null) ? "null" : function); // could try and sort returned functions if more than one based on // "best selection" for argument/parameter types if (function == null) { throw new IllegalArgumentException("Function maching signature and arguments not found"); } return function; } /** * Runs the function referenced by the "Function" element and returns its * value. * * @param lastValue * @return * @throws SQLException * @throws IllegalAccessException * @throws InvocationTargetException */ @Override public Object invoke(Object lastValue) throws SQLException, IllegalAccessException, InvocationTargetException, InstantiationException { log.debug("Function declaring class: {}", function.getDeclaringClass()); log.debug("Function: {}", function); ClassAndFunctionRegistry registry = ClassAndFunctionRegistry.singleton(); Object ob = registry.getFunctionsSingleton(function.getDeclaringClass()); if ( ob == null && lastValue != null && !Modifier.isStatic(function.getModifiers()) && !RequirementFunction.class.isAssignableFrom(function.getDeclaringClass()) && TypeConverter.isConvertible(lastValue.getClass(), function.getDeclaringClass()) ) { ob = TypeConverter.convert(lastValue, function.getDeclaringClass()); } java.lang.reflect.Parameter[] parameters = function.getParameters(); List<Object> fnArguments = new ArrayList<>(); final Map<String, Argument> mappedArgs = CollectionUtils.emptyIfNull(arguments).stream() .collect(Collectors.toMap(Argument::getName, (o) -> o, (x, y) -> x)); // because getValue(rs) throws exceptions, can't use stream map // lambda function int index = -1; for (java.lang.reflect.Parameter p : parameters) { ++index; log.debug("Looking for argument {} in {} or {} in {}", p.getName(), mappedArgs, index, arguments); Argument arg = arguments.get(index); NamedParameter named = p.getAnnotation(NamedParameter.class); if (named != null && mappedArgs.containsKey(named.value())) { arg = mappedArgs.get(named.value()); } else if (mappedArgs.containsKey(p.getName())) { arg = mappedArgs.get(p.getName()); } fnArguments.add(TypeConverter.convert( arg.getValue(lastValue), p.getType() )); } final Object fnOb = ob; log.debug("invoking: {} on: {} with: ({})", () -> function.getName(), () -> fnOb, () -> fnArguments.toString() ); return function.invoke(fnOb, fnArguments.toArray()); } }