/*
* Copyright 2014 Igor Maznitsa (http://www.igormaznitsa.com).
*
* 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.igormaznitsa.prol.libraries;
import com.igormaznitsa.prol.annotations.Determined;
import com.igormaznitsa.prol.annotations.Evaluable;
import com.igormaznitsa.prol.annotations.WrappedPredicate;
import com.igormaznitsa.prol.data.NumericTerm;
import com.igormaznitsa.prol.data.Term;
import com.igormaznitsa.prol.data.TermFloat;
import com.igormaznitsa.prol.data.TermInteger;
import com.igormaznitsa.prol.data.TermList;
import com.igormaznitsa.prol.data.TermStruct;
import com.igormaznitsa.prol.exceptions.ProlCriticalError;
import com.igormaznitsa.prol.logic.Goal;
import com.igormaznitsa.prol.logic.ProlContext;
import java.lang.reflect.Array;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* This class allows to wrap classes which are non successors of the
* ProlAbstractLibrary which can be used as prol libraries To sign a method as a
* predicate you need just to place the WrappedPredicate annotation before it.
* <pre>
* @WrappedPredicate
* public void helloworld(String str) {
* System.out.println(str);
* }
* </pre> The ProlLibraryWrapper will make automatic method argument marshaling.
* It supports primitive types, Strings, arrays, Objects, Map<Object> and
* Set<Object>
*
* @author Igor Maznitsa (igor.maznitsa@igormaznitsa.com)
* @see ProlAbstractLibrary
* @see WrappedPredicate
*/
public final class ProlLibraryWrapper extends ProlAbstractLibrary {
/**
* Inside logger which is addressed by
* Logger.getLogger(ProlLibraryWrapper.class.getCanonicalName())
*/
protected static final Logger LOG = Logger.getLogger(ProlLibraryWrapper.class.getCanonicalName());
/**
* The variable contains a wrapped object
*/
private final Object wrappedObject;
/**
* The method which will be used to handle evaluable predicates
*/
private static final Method EVAL_PREDICATE_HANDLER;
/**
* The method which will be used to handle other predicates
*/
private static final Method PREDICATE_HANDLER;
/**
* Inside map to link signature and methods of the wrapped object
*/
private final Map<String, Method> methodMap;
static {
try {
EVAL_PREDICATE_HANDLER = ProlLibraryWrapper.class.getDeclaredMethod("proxyEvaluablePredicate", Goal.class, TermStruct.class);
PREDICATE_HANDLER = ProlLibraryWrapper.class.getDeclaredMethod("proxyPredicate", Goal.class, TermStruct.class);
}
catch (SecurityException ex) {
LOG.throwing(ProlLibraryWrapper.class.getCanonicalName(), "static()", ex);
final Error error = new InternalError("Can't get needed method object for security restrictions");
LOG.throwing(ProlLibraryWrapper.class.getCanonicalName(), "static()", error);
throw error;
}
catch (NoSuchMethodException ex) {
LOG.throwing(ProlLibraryWrapper.class.getCanonicalName(), "static()", ex);
final Error error = new InternalError("Can't get find needed inside method object");
LOG.throwing(ProlLibraryWrapper.class.getCanonicalName(), "static()", error);
throw error;
}
}
/**
* The constructor, it is private because it must not be called outside
*
* @param libId the identifier of the library, must not be null
* @param wrappedObj the object to be wrapped, must not be null
*/
private ProlLibraryWrapper(final String libId, final Object wrappedObj) {
super(libId);
this.methodMap = new HashMap<String, Method>();
this.wrappedObject = wrappedObj;
fillPredicateTable();
}
/**
* Getter for the wrapped object
*
* @return the wrapped object
*/
public Object getWrappedObject() {
return wrappedObject;
}
/**
* The make a wrapper instance for a class or its instance
*
* @param wrappedObj a Class or an Object, must not be null
* @return a wrapper for the object as ProlLibraryWrapper
* @throws NullPointerException if the argument is null
*/
public static ProlLibraryWrapper makeWrapper(final Object wrappedObj) {
if (wrappedObj == null) {
throw new NullPointerException("You can't supply null as the argument");
}
final String newwrapperid = wrappedObj.toString();
LOG.log(Level.INFO, "Making wrapper for {0}", wrappedObj);
return new ProlLibraryWrapper(newwrapperid, wrappedObj);
}
/**
* Inside function to fill inside tables for handling
*/
@SuppressWarnings("unchecked")
private void fillPredicateTable() {
final boolean onlyStatic = wrappedObject instanceof Class;
final Class<?> classOfWrapped = onlyStatic ? (Class) wrappedObject : wrappedObject.getClass();
final Method[] methods = classOfWrapped.getDeclaredMethods();
for (int li = 0; li < methods.length; li++) {
final Method meth = methods[li];
if (meth.isAnnotationPresent(WrappedPredicate.class)) {
LOG.log(Level.INFO, "Detected wrapped predicate based on the method ''{0}''", new Object[]{meth.getName()});
boolean processTheMethod = true;
if (onlyStatic && !Modifier.isStatic(meth.getModifiers())) {
processTheMethod = false;
LOG.log(Level.WARNING, "Nonstatic wrapped predicate for the method ''{0}''", new Object[]{meth.getName()});
}
if (processTheMethod) {
final WrappedPredicate annot = meth.getAnnotation(WrappedPredicate.class);
String name = annot.Name().trim().toLowerCase();
if (name.length() == 0) {
// need to generate predicate name
name = generatePredicateNameFromMethodName(meth);
LOG.log(Level.WARNING, "There is not any predefined name so autogenerate ''{0}''", new Object[]{name});
}
else {
LOG.log(Level.INFO, "Detected the predefined name for a predicate ''{0}''", new Object[]{name});
}
if (!checkNameForValidity(name)) {
throw new IllegalArgumentException("Invalid predicate name detected \'" + name + "\' [" + meth.getName() + ']');
}
final Class<?>[] args = meth.getParameterTypes();
final Class<?> result = meth.getReturnType();
final boolean evaluable = result.isAssignableFrom(int.class) || result.isAssignableFrom(float.class);
if (!evaluable) {
// check for result
if (!result.isAssignableFrom(void.class) && !result.isAssignableFrom(boolean.class)) {
throw new IllegalArgumentException("Wrapped method \'" + meth.toGenericString() + "\' returns non void unsupported type [" + result.toString() + ']');
}
}
final String predicateSignature = name + '/' + args.length;
final PredicateProcessor predprocessor = new PredicateProcessor(this, name, evaluable ? EVAL_PREDICATE_HANDLER : PREDICATE_HANDLER, null);
if (predicateMethodsMap.put(predicateSignature, predprocessor) == null) {
// to map the method at inside mapper
methodMap.put(predicateSignature, meth);
}
else {
LOG.log(Level.WARNING, "Duplicated processor detected for predicate ''{0}''", new Object[]{predicateSignature});
final RuntimeException runtimeexception = new IllegalArgumentException("Duplicated processor for method \'" + meth.toString() + "\' signature \'" + predicateSignature + '\'');
LOG.throwing(this.getClass().getCanonicalName(), "Duplicated processor", runtimeexception);
throw runtimeexception;
}
LOG.log(Level.INFO, "Predicate processor for ''{0}'' has been created", predicateSignature);
}
}
}
}
/**
* Inside function to handle a predicate
*
* @param goal the goal which needs to solve the predicate, must not be null
* @param predicate the predicate (as a structure) to be solved, must not be
* null
* @return a NumericTerm if the predicate is an evaluable one in other case,
* either Boolean.TRUE if solved successfully or Boolean.FALSE if it failed or
* any exception was thrown during a handling of nonevaluable predicate
* @throws ProlCriticalError will be thrown if it is not possible to find
* mapped method or there was an Exception during handling of an evaluable
* predicate
*/
private Object handlePredicate(final Goal goal, final TermStruct predicate) {
final ProlContext context = goal.getContext();
final int arity = predicate.getArity();
final String signature = predicate.getSignature();
final Method mappedmethod = methodMap.get(signature);
if (mappedmethod == null) {
final ProlCriticalError error = new ProlCriticalError("Can't find mapped method for signature \'" + signature + '\'');
LOG.throwing(this.getClass().getCanonicalName(), "Can't find mapped method", error);
throw error;
}
final Class<?>[] argClasses = mappedmethod.getParameterTypes();
final Object[] args = new Object[arity];
for (int li = 0; li < arity; li++) {
args[li] = term2obj(context, argClasses[li], predicate.getElement(li));
}
Object result = Boolean.TRUE;
try {
result = mappedmethod.invoke(wrappedObject, args);
}
catch (Exception ex) {
LOG.log(Level.SEVERE, "Exception during method invoke [" + mappedmethod.toGenericString() + "]", ex);
if (mappedmethod.getReturnType().isAssignableFrom(Number.class)) {
// it's an evaluable term, we must throw error
throw new ProlCriticalError("Exception during a wrapped method was thrown", ex);
}
else {
// we return false if it is not evaluable term
result = Boolean.FALSE;
}
}
return result;
}
/**
* This method handles all wrapped evaluable predicates, these predicates have
* to return a result as a Number
*
* @param goal the goal to be solved, must not be null
* @param predicate the handled predicate, must not be null
* @return a NumericTerm fromed from the returned value of the mapped method,
* must not be null
* @throws NullPointerException it will be thrown if null was returrned by
* handled method
*/
@Evaluable
@Determined
protected Term proxyEvaluablePredicate(final Goal goal, final TermStruct predicate) {
final Number returned = (Number) handlePredicate(goal, predicate);
if (returned == null) {
final NullPointerException ex = new NullPointerException("A predicate, signed as an evaluable one, has returned null [" + predicate.toString() + ']');
LOG.log(Level.SEVERE, "Detected null as result", ex);
throw ex;
}
return goal.getContext().objectAsTerm(returned);
}
/**
* Handle a nonevaluable predicate
*
* @param goal the goal to be solved, must not be null
* @param predicate the handled predicate, must not be null
* @return true if all ok, false if the predicate failed
*/
@Determined
protected boolean proxyPredicate(final Goal goal, final TermStruct predicate) {
final Object obj = handlePredicate(goal, predicate);
boolean result = true;
if (obj instanceof Boolean) {
result = ((Boolean) obj).booleanValue();
}
return result;
}
/**
* An inside function to check validity of a formed name
*
* @param name the name to be checked,must not be null
* @return true if the name is ok, else false
* @throws NullPointerException will be thrown if the nargument is null
*/
private static boolean checkNameForValidity(final String name) {
if (name == null) {
throw new NullPointerException("Name can't be null");
}
if (name.length() == 0) {
return false;
}
final char firstChar = name.charAt(0);
if (Character.isWhitespace(firstChar)) {
return false;
}
if (Character.isDigit(firstChar)) {
return false;
}
return firstChar != '_';
}
/**
* To form a predicate name from the method name
*
* @param method the method which name will be used to form the predicate
* name, must not be null
* @return the formed name as String
* @throws IllegalArgumentException will be thrown if it is impossible to
* generate a valid name from the method name
*/
private static String generatePredicateNameFromMethodName(final Method method) {
String name = method.getName().toLowerCase();
while (true) {
if (name.charAt(0) == '_') {
name = name.substring(1);
}
else {
break;
}
}
if (name.length() == 0) {
throw new IllegalArgumentException("Can't make valid predicate name from the method name \'" + method.getName() + '\'');
}
return name;
}
/**
* Inside aux function to set an Object into a cell of an array, the array can
* have a primitive type
*
* @param array the target array, must not be null
* @param index the cell index at the array (0 or more)
* @param element the element which should be setted to the cell
* @throws ProlCriticalError will be thrown if the array type is not supported
*/
private static void setObjectToArrayElement(final Object array, final int index, final Object element) {
final Class<?> componentclass = array.getClass().getComponentType();
if (componentclass.isPrimitive()) {
if (componentclass == int.class) {
final int[] arr = (int[]) array;
arr[index] = (Integer) element;
}
else if (componentclass == float.class) {
final float[] arr = (float[]) array;
arr[index] = (Float) element;
}
else if (componentclass == byte.class) {
final byte[] arr = (byte[]) array;
arr[index] = (Byte) element;
}
else if (componentclass == char.class) {
final char[] arr = (char[]) array;
arr[index] = (Character) element;
}
else if (componentclass == short.class) {
final short[] arr = (short[]) array;
arr[index] = (Short) element;
}
else if (componentclass == double.class) {
final double[] arr = (double[]) array;
arr[index] = (Double) element;
}
else if (componentclass == boolean.class) {
final boolean[] arr = (boolean[]) array;
arr[index] = (Boolean) element;
}
else {
throw new ProlCriticalError("Unsupported primitive type [" + componentclass.getCanonicalName() + ']');
}
}
else {
((Object[]) array)[index] = element;
}
}
/**
* Generate new empty array for class (which can be a primitive type) for
* length
*
* @param type the class describes the type of new array
* @param length the length of new array
* @return new generated array as Object
* @throws ProlCriticalError if the type argument is not supported
*/
private static Object newArray(final Class<?> type, final int length) {
Object result = null;
if (type.isPrimitive()) {
if (type == int.class) {
result = new int[length];
}
else if (type == float.class) {
result = new float[length];
}
else if (type == long.class) {
result = new long[length];
}
else if (type == double.class) {
result = new double[length];
}
else if (type == byte.class) {
result = new byte[length];
}
else if (type == short.class) {
result = new short[length];
}
else if (type == char.class) {
result = new char[length];
}
else if (type == boolean.class) {
result = new boolean[length];
}
else {
throw new ProlCriticalError("Unsupported primitive type [" + type.getCanonicalName() + ']');
}
}
else {
result = Array.newInstance(type, length);
}
return result;
}
/**
* Complex inside function to automate conversion of a term into compatible
* representation which can be used as an object of a class type
*
* @param context the prol context which will be used for convertation, must
* not be null
* @param argclass the class of the result, must not be null
* @param term the term to be converted, must not be null
* @return an Object which is compatible with the argclass
* @throws ProlCriticalError will be thrown if there is an inside error during
* operation
*/
@SuppressWarnings("unchecked")
private static Object term2obj(final ProlContext context, final Class<?> argclass, final Term term) {
Object result = null;
if (argclass.isArray()) {
final Class<?> arrayclass = argclass.getComponentType();
switch (term.getTermType()) {
case Term.TYPE_LIST: {
final TermList list = (TermList) term;
if (list.isNullList()) {
result = newArray(arrayclass, 0);
}
else {
final int len = list.calculateLength();
final Object resultarr = newArray(arrayclass, len);
TermList lst = list;
int index = 0;
while (true) {
final Term head = lst.getHead();
setObjectToArrayElement(resultarr, index, term2obj(context, arrayclass, head));
index++;
final Term tail = lst.getTail();
if (tail.getTermType() == Term.TYPE_LIST) {
if (((TermList) tail).isNullList()) {
break;
}
else {
lst = (TermList) tail;
}
}
else {
setObjectToArrayElement(resultarr, index, term2obj(context, arrayclass, tail));
index++;
break;
}
}
if (len != index) {
throw new ProlCriticalError("Wrong converted array length detected [" + len + "!=" + index + ']');
}
result = resultarr;
}
}
break;
case Term.TYPE_STRUCT: {
final TermStruct struct = (TermStruct) term;
final int arity = struct.getArity();
final Object resultarr = newArray(arrayclass, arity + 1);
setObjectToArrayElement(resultarr, 0, term2obj(context, arrayclass, struct.getFunctor()));
for (int li = 0; li < arity; li++) {
setObjectToArrayElement(resultarr, li + 1, term2obj(context, arrayclass, struct.getElement(li)));
}
result = resultarr;
}
break;
default: {
final Object resultarr = newArray(arrayclass, 1);
setObjectToArrayElement(resultarr, 0, term2obj(context, arrayclass, term));
result = resultarr;
}
break;
}
}
else {
if (argclass.isPrimitive()) {
if (argclass == int.class) {
if (term instanceof TermInteger) {
result = ((TermInteger) term).getNumericValue();
}
}
else if (argclass == long.class) {
if (term instanceof TermInteger) {
result = Long.valueOf(((TermInteger) term).getNumericValue().intValue());
}
}
else if (argclass == double.class) {
if (term instanceof TermInteger) {
result = Double.valueOf(((TermInteger) term).getNumericValue().doubleValue());
}
else if (term instanceof TermFloat) {
result = Double.valueOf(((TermFloat) term).getNumericValue().doubleValue());
}
}
else if (argclass == float.class) {
if (term instanceof TermInteger) {
result = Float.valueOf(((TermInteger) term).getNumericValue().floatValue());
}
else if (term instanceof TermFloat) {
result = ((TermFloat) term).getNumericValue();
}
}
else if (argclass == boolean.class) {
if (term instanceof NumericTerm) {
result = ((NumericTerm) term).getNumericValue().intValue() != 0;
}
else {
result = "true".equalsIgnoreCase(term.getText());
}
}
else if (argclass == byte.class) {
if (term instanceof NumericTerm) {
result = Byte.valueOf(((NumericTerm) term).getNumericValue().byteValue());
}
}
else if (argclass == char.class) {
if (term instanceof NumericTerm) {
result = Character.valueOf((char) ((NumericTerm) term).getNumericValue().shortValue());
}
else if (term.getTermType() == Term.TYPE_ATOM) {
final String text = term.getText();
if (text.length() == 1) {
result = text.charAt(0);
}
}
}
else if (argclass == short.class) {
if (term instanceof NumericTerm) {
result = Short.valueOf(((NumericTerm) term).getNumericValue().shortValue());
}
}
else {
throw new ProlCriticalError("Unsupported primitive type [" + argclass.getCanonicalName() + ']');
}
}
else {
if (argclass == Object.class) {
result = context.termAsObject(term);
}
else if (argclass == String.class) {
result = term.getText();
}
else if (argclass == List.class) {
result = Arrays.asList((Object[]) term2obj(context, (new Object[0]).getClass(), term));
}
else if (argclass == Set.class) {
final Object[] asarray = (Object[]) term2obj(context, (new Object[0]).getClass(), term);
final Set<Object> resultset = new HashSet<Object>();
resultset.addAll(Arrays.asList(asarray));
result = resultset;
}
else {
throw new ProlCriticalError("Unsupported type [" + argclass.getCanonicalName() + ']');
}
}
}
if (result == null) {
throw new ProlCriticalError("Can't convert \'" + term.toString() + "\' to \'" + argclass.getCanonicalName() + "\' compatible representation");
}
return result;
}
}