code: package com.taxonic.carml.engine.function;
  
  import com.taxonic.carml.rdf_mapper.util.ImmutableCollectors;
  import java.lang.reflect.InvocationTargetException;
  import java.lang.reflect.Method;
  import java.lang.reflect.Parameter;
  import java.lang.reflect.Type;
  import java.util.Arrays;
  import java.util.Collection;
  import java.util.LinkedHashMap;
  import java.util.List;
  import java.util.Map;
  import java.util.Optional;
  import java.util.function.Function;
  import java.util.function.UnaryOperator;
  import java.util.stream.Collectors;
  import org.eclipse.rdf4j.model.IRI;
  import org.eclipse.rdf4j.model.Literal;
  import org.eclipse.rdf4j.model.Model;
  import org.eclipse.rdf4j.model.Resource;
  import org.eclipse.rdf4j.model.Statement;
  import org.eclipse.rdf4j.model.Value;
  import org.eclipse.rdf4j.model.ValueFactory;
  import org.eclipse.rdf4j.model.impl.SimpleValueFactory;
  import org.slf4j.Logger;
  import org.slf4j.LoggerFactory;
  
  public class Functions {
  
  	private static final Logger LOG = LoggerFactory.getLogger(Functions.class);
  
  	private static final ValueFactory VF = SimpleValueFactory.getInstance();
  
  	private Map<IRI, ExecuteFunction> fns = new LinkedHashMap<>();
  
  	public Optional<ExecuteFunction> getFunction(IRI iri) {
  		return Optional.ofNullable(fns.get(iri));
  	}
  
  	public void addFunctions(Object... functions) {
  		for (Object fn : functions) {
  			Arrays.asList(fn.getClass().getMethods())
  				.stream()
  				.map(m -> createFunctionExecutor(fn, m))
  				.filter(Optional::isPresent)
  				.map(Optional::get)
  				.forEach(f -> fns.put(f.getIri(), f));
  		}
  	}
  
  	private Optional<ExecuteFunction> createFunctionExecutor(Object obj, Method method) {
  
  		FnoFunction function = method.getAnnotation(FnoFunction.class);
  		if (function == null) return Optional.empty();
  		IRI iri = VF.createIRI(function.value());
  
  		List<ExtractParameter> parameterExtractors =
  			Arrays.asList(method.getParameters())
  				.stream()
  				.map(this::createParameterExtractor)
  				.collect(Collectors.toList());
  
  		LOG.debug("Creating executable FnO function {}", function);
  		return Optional.of(new ExecuteFunction() {
  
  			@Override
  			public Object execute(Model model, Resource subject, UnaryOperator<Object> returnValueAdapter) {
  
  				List<Object> arguments = parameterExtractors
  					.stream()
  					.map(e -> e.extract(model, subject))
  					.collect(Collectors.toList());
  
  				try {
  					if (LOG.isTraceEnabled()) {
  						LOG.trace("Executing function {} with arguments {}", method.getName(), arguments);
  					}
  
  					Object returnValue = method.invoke(obj, arguments.toArray());
  
  					if (returnValue == null) {
  						return null;
  					}
  
  					return returnValueAdapter.apply(returnValue);
  				}
  				catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
  					throw new RuntimeException("error executing function", e);
  				}
  			}
  
  			@Override
  			public IRI getIri() {
  				return iri;
  			}
  		});
  	}
  
  	private ExtractParameter createParameterExtractor(Parameter parameter) {
  
  		FnoParam param = parameter.getAnnotation(FnoParam.class);
  		if (param == null)
  			throw new RuntimeException("no @" + FnoParam.class.getName() +
  				" annotation present on parameter");
  		IRI iri = VF.createIRI(param.value());
  
  		Type type = parameter.getType();
  
  		Function<List<Value>, Object> adapter;
  
  		if (type.equals(Integer.TYPE) || type.equals(Integer.class)) {
  			adapter = l -> {
  				if (l == null || l.isEmpty()) {
  					// Return null for empty function parameter
  					return null;
  				}
  
  				expectSingleValue(l);
  				return literalToInt(l.get(0));
  			};
  		}
  
  		else if (type.equals(String.class)) {
  			adapter = l -> {
  				if (l == null || l.isEmpty()) {
  					// Return null for empty function parameter
  					return null;
  				}
  
  				expectSingleValue(l);
  				return literalToString(l.get(0));
  			};
  		}
  
  		else if (type.equals(Double.TYPE) || type.equals(Double.class)) {
  			adapter = l -> {
  				if (l == null || l.isEmpty()) {
  					// Return null for empty function parameter
  					return null;
  				}
  
  				expectSingleValue(l);
  				return literalToDouble(l.get(0));
  			};
  		}
  
  		else if (type.equals(Float.TYPE) || type.equals(Float.class)) {
  			adapter = l -> {
  				if (l == null || l.isEmpty()) {
  					// Return null for empty function parameter
  					return null;
  				}
  
  				expectSingleValue(l);
  				return literalToFloat(l.get(0));
  			};
  		}
  
  		else if (type.equals(Long.TYPE) || type.equals(Long.class)) {
  			adapter = l -> {
  				if (l == null || l.isEmpty()) {
  					// Return null for empty function parameter
  					return null;
  				}
  
  				expectSingleValue(l);
  				return literalToLong(l.get(0));
  			};
  		}
  
  		else if (type.equals(Boolean.TYPE) || type.equals(Boolean.class)) {
  			adapter = l -> {
  				if (l == null || l.isEmpty()) {
  					// Return null for empty function parameter
  					return null;
  				}
  
  				expectSingleValue(l);
  				return literalToBoolean(l.get(0));
  			};
  		}
  
  		else if (Collection.class.isAssignableFrom(parameter.getType())) {
  			// TODO: Currently only collections with string parameter type supported.
  			adapter = l -> {
  				if (l == null || l.isEmpty()) {
  					// Return null for empty function parameter
  					return null;
  				}
  				if (!(l instanceof Collection)) {
  					throw new IllegalArgumentException(
  						"value [" + l + "] was not a collection, which is expected " +
  						"for a parameter of type Collection<?>"
  					);
  				}
  
  				return l.stream()
  						.map(Value::stringValue)
  						.collect(ImmutableCollectors.toImmutableList());
  			};
  		}
  
  		else
  			throw new RuntimeException("parameter type [" + type + "] not (yet) supported");
  
  		return new ExtractParameter() {
  
  			@Override
  			public Object extract(Model model, Resource subject) {
  				Model paramValues = model.filter(subject, iri, null);
  
  				List<Value> values = paramValues.stream()
  				.map(Statement::getObject)
  				.collect(ImmutableCollectors.toImmutableList());
  
  				return adapter.apply(values);
  			}
  		};
  	}
  
  	public int size() {
  		return fns.size();
  	}
  
  	private void expectSingleValue(List<Value> values) {
  		if (values.size() > 1) {
  			throw new IllegalArgumentException(
  				"value [" + values + "] has more than one value, which is not expected."
  			);
  		}
  	}
  
  	private String literalToString(Value v) {
  		if (!(v instanceof Literal)) {
  			throw new IllegalArgumentException(
  				"value [" + v + "] was not a literal, which is expected " +
  				"for a parameter of type String."
  			);
  		}
  		Literal literal = (Literal) v;
  		return literal.stringValue();
  	}
  
  	private int literalToInt(Value v) {
  		if (!(v instanceof Literal)) {
  			throw new IllegalArgumentException(
  				"value [" + v + "] was not a literal, which is expected " +
  				"for a parameter of type int or Integer."
  			);
  		}
  		Literal literal = (Literal) v;
  		return literal.intValue();
  	}
  
  	private double literalToDouble(Value v) {
  		if (!(v instanceof Literal)) {
  			throw new IllegalArgumentException(
  				"value [" + v + "] was not a literal, which is expected " +
  				"for a parameter of type double or Double."
  			);
  		}
  		Literal literal = (Literal) v;
  
  		return literal.doubleValue();
  	}
  
  	private float literalToFloat(Value v) {
  		if (!(v instanceof Literal)) {
  			throw new IllegalArgumentException(
  				"value [" + v + "] was not a literal, which is expected " +
  				"for a parameter of type float or Float."
  			);
  		}
  		Literal literal = (Literal) v;
  
  		return literal.floatValue();
  	}
  
  	private long literalToLong(Value v) {
  		if (!(v instanceof Literal)) {
  			throw new IllegalArgumentException(
  				"value [" + v + "] was not a literal, which is expected " +
  				"for a parameter of type long or Long."
  			);
  		}
  		Literal literal = (Literal) v;
  
  		return literal.longValue();
  	}
  
  	private boolean literalToBoolean(Value v) {
  		if (!(v instanceof Literal)) {
  			throw new IllegalArgumentException(
  				"value [" + v + "] was not a literal, which is expected " +
  				"for a parameter of type boolean or Boolean."
  			);
  		}
  		Literal literal = (Literal) v;
  
  		return literal.booleanValue();
  	}
  
  }
  