InternalAnnotator.java
/*
* Copyright 2017 Michael Mackenzie High
*
* 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.mackenziehigh.sexpr.internal.schema;
import com.mackenziehigh.sexpr.SAtom;
import com.mackenziehigh.sexpr.SList;
import com.mackenziehigh.sexpr.Schema;
import com.mackenziehigh.sexpr.Schema.Builder;
import com.mackenziehigh.sexpr.Sexpr;
import com.mackenziehigh.sexpr.annotations.After;
import com.mackenziehigh.sexpr.annotations.Before;
import com.mackenziehigh.sexpr.annotations.Condition;
import com.mackenziehigh.sexpr.annotations.Pass;
import java.lang.reflect.Method;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
/**
*
*/
public final class InternalAnnotator
{
private final Schema.Builder builder;
public InternalAnnotator (final Builder builder)
{
this.builder = builder;
}
/**
* Given an object containing properly annotated methods,
* define the conditions and actions defined therein.
*
* @param object contains condition and action definitions.
*/
public void defineViaReflection (final Object object)
{
final Optional<String> defaultPass = getDefaultPass(object);
for (Method method : object.getClass().getMethods())
{
if (method.isAnnotationPresent(Condition.class))
{
defineConditionViaReflection(object, method);
}
if (method.isAnnotationPresent(Before.class))
{
defineBeforeActionByReflection(defaultPass, object, method);
}
if (method.isAnnotationPresent(After.class))
{
defineAfterActionByReflection(defaultPass, object, method);
}
}
}
private Optional<String> getDefaultPass (final Object object)
{
if (object.getClass().isAnnotationPresent(Pass.class))
{
final String name = object.getClass().getAnnotation(Pass.class).value();
return Optional.of(name);
}
else
{
return Optional.empty();
}
}
private String getPass (final Optional<String> defaultPass,
final Method method)
{
if (method.isAnnotationPresent(Pass.class))
{
final String name = method.getAnnotation(Pass.class).value();
return name;
}
else if (defaultPass.isPresent())
{
return defaultPass.get();
}
else
{
throw new IllegalArgumentException("No translation pass was specified on either the class or method.");
}
}
private void defineConditionViaReflection (final Object object,
final Method method)
{
/**
* Obtain the user-defined name of the condition.
*/
final String name = method.getAnnotation(Condition.class).value();
/**
* The method cannot throw any checked exceptions.
*/
if (method.getExceptionTypes().length != 0)
{
final String message = String.format("Do *not* throw checked exceptions in (%s).", method.toString());
throw new IllegalArgumentException(message);
}
/**
* The return-type of the method must be boolean.
*/
if (method.getReturnType().equals(boolean.class) == false)
{
final String message = String.format("You must return boolean from (%s), not %s.",
method.toString(),
method.getReturnType().getName());
throw new IllegalArgumentException(message);
}
/**
* The method must take exactly one argument,
* which must be a symbolic-expression.
*/
if (method.getParameterCount() != 1)
{
final String message = String.format("Method (%s) must take exactly one parameter.", method.toString());
throw new IllegalArgumentException(message);
}
else if (method.getParameterTypes()[0].equals(Sexpr.class))
{
final Function<Sexpr, Object> invocation = createInvocation(object, method);
final Predicate<Sexpr<?>> condition = x -> (Boolean) invocation.apply(x);
builder.condition(name, condition);
}
else if (method.getParameterTypes()[0].equals(SAtom.class))
{
final Function<SAtom, Object> invocation = createInvocation(object, method);
final Predicate<Sexpr<?>> condition = x -> x.isAtom() ? (Boolean) invocation.apply(x.asAtom()) : false;
builder.condition(name, condition);
}
else if (method.getParameterTypes()[0].equals(SList.class))
{
final Function<SList, Object> invocation = createInvocation(object, method);
final Predicate<Sexpr<?>> condition = x -> x.isList() ? (Boolean) invocation.apply(x.asList()) : false;
builder.condition(name, condition);
}
else
{
final String message = String.format("Method (%s) must take a %s|%s|%s as its only parameter.",
method.toString(),
Sexpr.class.getName(),
SAtom.class.getName(),
SList.class.getName());
throw new IllegalArgumentException(message);
}
}
private void defineBeforeActionByReflection (final Optional<String> defaultPass,
final Object object,
final Method method)
{
/**
* Obtain the name of the translation pass that this action applies to.
*/
final String pass = getPass(defaultPass, method);
/**
* Obtain the user-defined name of the rule that this action applies to.
*/
final String rule = method.getAnnotation(Before.class).value();
/**
* The method cannot throw any checked exceptions.
*/
if (method.getExceptionTypes().length != 0)
{
final String message = String.format("Do *not* throw checked exceptions in (%s).", method.toString());
throw new IllegalArgumentException(message);
}
/**
* The return-type of the method must be void.
*/
if (method.getReturnType().equals(void.class) == false)
{
final String message = String.format("You must return boolean from (%s), not %s.",
method.toString(),
method.getReturnType().getName());
throw new IllegalArgumentException(message);
}
/**
* The method must take exactly one argument,
* which must be a symbolic-expression.
*/
if (method.getParameterCount() != 1)
{
final String message = String.format("Method (%s) must take exactly one parameter.", method.toString());
throw new IllegalArgumentException(message);
}
else if (method.getParameterTypes()[0].equals(Sexpr.class))
{
final Function<Sexpr<?>, Object> invocation = createInvocation(object, method);
final Consumer<Sexpr<?>> action = x -> invocation.apply(x);
builder.before(pass, rule, action);
}
else if (method.getParameterTypes()[0].equals(SAtom.class))
{
final Function<SAtom, Object> invocation = createInvocation(object, method);
final Consumer<Sexpr<?>> action = x -> invocation.apply(x.asAtom());
builder.before(pass, rule, action);
}
else if (method.getParameterTypes()[0].equals(SList.class))
{
final Function<SList, Object> invocation = createInvocation(object, method);
final Consumer<Sexpr<?>> action = x -> invocation.apply(x.asList());
builder.before(pass, rule, action);
}
else
{
final String message = String.format("Method (%s) must take a %s|%s|%s as its only parameter.",
method.toString(),
Sexpr.class.getName(),
SAtom.class.getName(),
SList.class.getName());
throw new IllegalArgumentException(message);
}
}
private void defineAfterActionByReflection (final Optional<String> defaultPass,
final Object object,
final Method method)
{
/**
* Obtain the name of the translation pass that this action applies to.
*/
final String pass = getPass(defaultPass, method);
/**
* Obtain the user-defined name of the rule that this action applies to.
*/
final String rule = method.getAnnotation(After.class).value();
/**
* The method cannot throw any checked exceptions.
*/
if (method.getExceptionTypes().length != 0)
{
final String message = String.format("Do *not* throw checked exceptions in (%s).", method.toString());
throw new IllegalArgumentException(message);
}
/**
* The return-type of the method must be void.
*/
if (method.getReturnType().equals(void.class) == false)
{
final String message = String.format("You must return boolean from (%s), not %s.",
method.toString(),
method.getReturnType().getName());
throw new IllegalArgumentException(message);
}
/**
* The method must take exactly one argument,
* which must be a symbolic-expression.
*/
if (method.getParameterCount() != 1)
{
final String message = String.format("Method (%s) must take exactly one parameter.", method.toString());
throw new IllegalArgumentException(message);
}
else if (method.getParameterTypes()[0].equals(Sexpr.class))
{
final Function<Sexpr<?>, Object> invocation = createInvocation(object, method);
final Consumer<Sexpr<?>> action = x -> invocation.apply(x);
builder.after(pass, rule, action);
}
else if (method.getParameterTypes()[0].equals(SAtom.class))
{
final Function<SAtom, Object> invocation = createInvocation(object, method);
final Consumer<Sexpr<?>> action = x -> invocation.apply(x.asAtom());
builder.after(pass, rule, action);
}
else if (method.getParameterTypes()[0].equals(SList.class))
{
final Function<SList, Object> invocation = createInvocation(object, method);
final Consumer<Sexpr<?>> action = x -> invocation.apply(x.asList());
builder.after(pass, rule, action);
}
else
{
final String message = String.format("Method (%s) must take a %s|%s|%s as its only parameter.",
method.toString(),
Sexpr.class.getName(),
SAtom.class.getName(),
SList.class.getName());
throw new IllegalArgumentException(message);
}
}
private <T> Function<T, Object> createInvocation (final Object object,
final Method method)
{
final Function<T, Object> function = x ->
{
try
{
method.setAccessible(true);
return method.invoke(object, x);
}
catch (Throwable ex)
{
throw new RuntimeException(x.toString(), ex);
}
};
return function;
}
}