org.logic2j.core.impl.DefaultTermAdapter.java Source code

Java tutorial

Introduction

Here is the source code for org.logic2j.core.impl.DefaultTermAdapter.java

Source

/*
 * logic2j - "Bring Logic to your Java" - Copyright (C) 2011 Laurent.Tettoni@gmail.com
 *
 * This library 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 library 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.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 */
package org.logic2j.core.impl;

import org.apache.commons.beanutils.PropertyUtils;
import org.logic2j.core.api.TermAdapter;
import org.logic2j.core.api.TermMapper;
import org.logic2j.core.api.model.exception.InvalidTermException;
import org.logic2j.core.api.model.term.Struct;
import org.logic2j.core.api.model.term.TermApi;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.reflect.InvocationTargetException;
import java.util.*;
import java.util.Map.Entry;

/**
 * Default and reference implementation of {@link TermAdapter}.
 */
public class DefaultTermAdapter implements TermAdapter {
    private static final Logger logger = LoggerFactory.getLogger(DefaultTermAdapter.class);
    private final TermMapper NOOP_MAPPER = new TermMapper() {

        /**
         * Identity - return its argument.
         * @param theTerm
         * @return theTerm
         */
        @Override
        public Object apply(Object theTerm) {
            return theTerm;
        }
    };

    // FIXME use the current Prolog's configured TermMarshaller, not the default one!
    public static final DefaultTermMarshaller DEFAULT_TERM_MARSHALLER = new DefaultTermMarshaller();

    private IdentityHashMap<String, Object> predefinedAtoms = null;

    private TermMapper normalizer = NOOP_MAPPER;

    private final EnvManager envManager = new EnvManager();

    // TODO be smarter to handle Arrays and Collections, and Iterables
    @Override
    public Object toTerm(Object theObject, FactoryMode theMode) {
        // TODO Temporary just for compatibility - move this to TermUnmarshaller
        if (theObject instanceof CharSequence) {
            if (theMode == FactoryMode.ATOM) {
                final String string = theObject.toString();
                if (this.predefinedAtoms != null) {
                    final Object predefinedAtom = this.predefinedAtoms.get(string);
                    if (predefinedAtom != null) {
                        return predefinedAtom;
                    }
                }
                return Struct.atom(string);
            }
            throw new UnsupportedOperationException(
                    "TermAdapter cannot parse complex CharSequences, use TermUnmarshaller instead");
        }
        final Object created = toTermInternal(theObject, theMode);
        final Object normalized = normalizer.apply(created);
        return normalized;
    }

    @Override
    public Struct toStruct(String thePredicateName, FactoryMode theMode, Object... theArguments) {
        final Object[] convertedArgs = new Object[theArguments.length];
        for (int i = 0; i < theArguments.length; i++) {
            convertedArgs[i] = toTermInternal(theArguments[i], theMode);
        }
        final Struct created = new Struct(thePredicateName, convertedArgs);
        final Struct normalized = (Struct) normalizer.apply(created);
        return normalized;
    }

    /**
     * @return A List of one single Term from {@link #toTerm(Object, org.logic2j.core.api.TermAdapter.FactoryMode)}.
     */
    @Override
    public List<Object> toTerms(Object theObject, AssertionMode theAssertionMode) {
        final List<Object> result = new ArrayList<Object>();
        final Object term = toTerm(theObject, FactoryMode.ATOM);
        result.add(term);
        return result;
    }

    /**
     * Factory that can be overridden.
     * 
     * @param theObject
     * @param theMode
     * @return An instance of Term
     */
    private Object toTermInternal(Object theObject, FactoryMode theMode) {
        Object result = null;
        if (theObject == null) {
            if (theMode == FactoryMode.ATOM) {
                result = Struct.atom(""); // The empty string atom, see note on FactoryMode.ATOM
            } else {
                throw new InvalidTermException("Cannot create Term from a null argument");
            }
        }
        if (theObject instanceof CharSequence || theObject instanceof Character) {
            // Rudimentary parsing
            final String chars = String.valueOf(theObject);
            if (theMode == FactoryMode.ATOM) {
                // Anything becomes an atom, actually only a Struct since we don't have powerful parsing here
                // result = new Struct(chars);
                result = Struct.atom(chars);
            }
        }
        // Otherwise apply basic algorithm from TermApi
        if (result == null) {
            result = TermApi.valueOf(theObject, theMode);
        }
        return result;
    }

    /**
     * Default conversion allows for:
     * - exact matching
     * - castable interface or subclass
     * - String and CharSequence
     * - Enum  (handles exact value 'VAL' that must match Enum.name(), or any_wrapping_functor('VAL')
     * @param theTerm
     * @param theTargetClass
     * @param <T>
     * @return
     */
    @Override
    public <T> T fromTerm(Object theTerm, Class<T> theTargetClass) {
        if (theTerm == null) {
            // Pass-through for nulls
            return null;
        }
        final Class<?> termClass = theTerm.getClass();
        if (termClass == theTargetClass) {
            // Exact class
            return (T) theTerm;
        }
        if (theTargetClass.isAssignableFrom(termClass)) {
            // Allowed cast as per class hierarchy or interface implementation
            return (T) theTerm;
        }
        if (theTargetClass == String.class || theTargetClass == CharSequence.class) {
            if (theTerm instanceof Struct) {
                return (T) DEFAULT_TERM_MARSHALLER.marshall(theTerm).toString();
            }
            return (T) String.valueOf(theTerm);
        }
        if (theTerm instanceof Struct && theTargetClass == List.class) {
            final Collection<?> collection = ((Struct) theTerm).javaListFromPList(null, Object.class);
            return (T) collection;
        }

        // Now these conversions are getting a bit rare. Prepare error message because it is likely we end up in error
        final String termDescription = "term \"" + theTerm + "\" of " + termClass;
        String adapterInstanceName = this.toString();
        // For anonymous classes we end up with "" !
        if (adapterInstanceName.isEmpty()) {
            adapterInstanceName = TermAdapter.class.getSimpleName();
        }
        final String message = adapterInstanceName + " cannot convert " + termDescription + " to " + theTargetClass;

        if (Enum.class.isAssignableFrom(theTargetClass)) {
            return (T) fromEnum(theTerm, (Class<Enum>) theTargetClass, message);
        }
        throw new UnsupportedOperationException(message);
    }

    /**
     * Specific conversion of enums
     * @param theTerm The Prolog term
     * @param theTargetClass The target Java class
     * @param message
     * @param <T>
     * @return
     */
    protected <T extends Enum> T fromEnum(Object theTerm, Class<T> theTargetClass, String message) {
        if (theTargetClass == Enum.class) {
            throw new IllegalArgumentException(
                    message + ": converting to any Enum will require a custom TermAdapter, " + this
                            + " cannot guess your intended Enum class");
        }

        // For converting to Enum, we expect that theTerm will match the name() of the target Enum.
        // We will allow theTerm to be either the exact atom (eg 'VAL'), or
        // a struct/1 with any functor and the value, (eg my_enum_type_ignored('VAL')).
        // The reason is for consistency with the cases when the Prolog will return an enum value of a class that
        // Java cannot pre-determine. In that case user has to override this method in a dedicated TermAdapter,
        // and lookup the effective enum from the specified functor.

        final String effectiveEnumName;
        if (theTerm instanceof String) {
            effectiveEnumName = theTerm.toString();
        } else if (theTerm instanceof Struct) {
            Struct s = (Struct) theTerm;
            if (s.getArity() != 1) {
                throw new IllegalArgumentException(
                        message + ": if a Struct is passed, arity must be 1, was " + s.getArity());
            }
            effectiveEnumName = s.getArg(0).toString();
        } else {
            throw new IllegalArgumentException(
                    message + ": converting to an Enum requires either an atom or a struct of arity=1");
        }

        final Enum[] enumConstants = theTargetClass.getEnumConstants();
        for (Enum c : enumConstants) {
            if (c.name().equals(effectiveEnumName)) {
                return (T) c;
            }
        }
        throw new IllegalArgumentException(message + ": no such enum value");
    }

    /**
     * Allow changing default behaviour of {@link #toTermInternal(Object, org.logic2j.core.api.TermAdapter.FactoryMode)} when FactoryMode is ATOM,
     * and the first argument exactly matches one of the keys of the map: will return the value
     * as the atom.
     * 
     * @param theAtoms
     */
    public void setPredefinedAtoms(Map<String, Object> theAtoms) {
        this.predefinedAtoms = new IdentityHashMap<String, Object>(theAtoms.size());
        for (final Entry<String, Object> entry : theAtoms.entrySet()) {
            this.predefinedAtoms.put(entry.getKey().intern(), entry.getValue());
        }
    }

    public void setNormalizer(TermMapper normalizer) {
        this.normalizer = normalizer;
    }

    @Override
    public Object getVariable(String theExpression) {
        return this.envManager.getVariable(theExpression);
    }

    @Override
    public TermAdapter setVariable(String theExpression, Object theValue) {
        this.envManager.setVariable(theExpression, theValue);
        return this;
    }

    @Override
    public String toString() {
        return this.getClass().getSimpleName();
    }
}