com.abyala.decisiontree.SimpleDecisionTreeParser.java Source code

Java tutorial

Introduction

Here is the source code for com.abyala.decisiontree.SimpleDecisionTreeParser.java

Source

/*
 * Copyright (c) 2012 Andrew Byala
 *
 * This file is part of DecisionTree.
 *
 * DecisionTree is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * DecisionTree 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with DecisionTree.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.abyala.decisiontree;

import com.abyala.decisiontree.exceptions.DecisionTreeParserException;
import com.abyala.decisiontree.nodes.Node;
import com.abyala.decisiontree.nodes.NodeBuilder;
import com.abyala.decisiontree.nodes.ResultNode;
import com.abyala.decisiontree.results.*;
import com.abyala.decisiontree.types.BooleanInputType;
import com.abyala.decisiontree.types.InputType;
import com.abyala.decisiontree.types.IntegerInputType;
import com.abyala.decisiontree.types.StringInputType;
import org.jdom2.Attribute;
import org.jdom2.Document;
import org.jdom2.Element;
import org.jdom2.JDOMException;
import org.jdom2.input.SAXBuilder;

import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.lang.reflect.Method;
import java.util.*;

/**
 * User: abyala
 * Date: 6/14/12
 */
public class SimpleDecisionTreeParser implements DecisionTreeParser {

    protected DecisionTree parseDocument(final Document doc) throws DecisionTreeParserException {
        final Element rootElement = doc.getRootElement();
        final Map<String, InputType> types = parseInputTypes(rootElement.getChild("input-types"));
        final ResultSpec resultSpec = parseResultClass(rootElement.getChild("result-type"));
        final Node rootNode = parseTable(rootElement.getChild("tree"), types, resultSpec);
        return new DecisionTree(rootNode, types.values());
    }

    private ResultSpec parseResultClass(final Element resultElement) throws DecisionTreeParserException {
        if (resultElement == null)
            throw new DecisionTreeParserException("Missing result-type element");

        final String className = resultElement.getAttributeValue("class");
        final Class clazz;
        try {
            clazz = Class.forName(className);
        } catch (ClassNotFoundException e) {
            throw new DecisionTreeParserException("Result class not found: " + className);
        }

        final ResultSpec.Builder builder = new ResultSpec.Builder(clazz);
        parseResultAttributes(builder, resultElement, clazz);
        return builder.build();
    }

    private Map<String, Method> parseAnnotatedMethods(final Class clazz) {
        final Map<String, Method> methodMap = new HashMap<String, Method>();

        for (Method method : clazz.getMethods()) {
            final ResultField annotation = method.getAnnotation(ResultField.class);
            if (annotation != null) {
                methodMap.put(annotation.name(), method);
            }
        }

        return methodMap;
    }

    private void parseResultAttributes(final ResultSpec.Builder builder, final Element element,
            final Class resultClass) throws DecisionTreeParserException {
        final Map<String, Method> annotatedResultMethods = parseAnnotatedMethods(resultClass);

        for (Element child : element.getChildren()) {
            final String attributeName = child.getAttributeValue("name");
            final Method method = annotatedResultMethods.get(attributeName);
            if (method == null)
                throw new DecisionTreeParserException("Result attribute \"" + attributeName
                        + "\" not defined on result class " + resultClass.getName());

            final ResultAttribute attribute = parseResultAttribute(child, method);
            builder.addAttribute(attribute);
        }
    }

    private ResultAttribute parseResultAttribute(final Element element, final Method method)
            throws DecisionTreeParserException {
        final String elementType = element.getName();
        final String attributeName = element.getAttributeValue("name");
        if ("string-attribute".equals(elementType)) {
            return parseResultStringAttribute(element, attributeName, method);
        } else if ("boolean-attribute".equals(elementType)) {
            return parseResultBooleanAttribute(element, attributeName, method);
        } else if ("text-attribute".equals(elementType)) {
            return parseResultTextAttribute(element, attributeName, method);
        } else if ("integer-attribute".equals(elementType)) {
            return parseResultIntegerAttribute(element, attributeName, method);
        } else {
            throw new DecisionTreeParserException("Unknown result attribute type: " + elementType);
        }
    }

    private ResultAttribute parseResultTextAttribute(final Element element, final String name,
            final Method method) {
        final TextResultAttribute.Builder builder = new TextResultAttribute.Builder(name, method);
        builder.setDefaultValue(element.getAttributeValue("default"));
        return builder.build();
    }

    private ResultAttribute parseResultBooleanAttribute(final Element element, final String name,
            final Method method) throws DecisionTreeParserException {
        final BooleanResultAttribute.Builder builder = new BooleanResultAttribute.Builder(name, method);
        final Attribute defaultAttribute = element.getAttribute("default");
        if (defaultAttribute != null) {
            final String value = defaultAttribute.getValue();
            if ("true".equals(value)) {
                builder.setDefaultValue(true);
            } else if ("false".equals(value)) {
                builder.setDefaultValue(false);
            } else {
                throw new DecisionTreeParserException(
                        "Invalid default value \"" + value + "\" for boolean result attribute \"" + name + "\"");
            }
        }

        return builder.build();
    }

    private StringResultAttribute parseResultStringAttribute(final Element element, final String name,
            final Method method) throws DecisionTreeParserException {
        final StringResultAttribute.Builder builder = new StringResultAttribute.Builder(name, method);
        for (Element value : element.getChildren("value")) {
            final String text = value.getTextNormalize();
            builder.addEnumValue(text);
            if ("true".equals(value.getAttributeValue("default"))) {
                builder.setDefaultValue(text);
            }
        }

        return builder.build();
    }

    private ResultAttribute parseResultIntegerAttribute(final Element element, final String name,
            final Method method) throws DecisionTreeParserException {
        final IntegerResultAttribute.Builder builder = new IntegerResultAttribute.Builder(name, method);
        final Attribute defaultAttribute = element.getAttribute("default");
        if (defaultAttribute != null) {
            final String value = defaultAttribute.getValue();
            try {
                final Integer intValue = Integer.parseInt(value);
                builder.setDefaultValue(intValue);
            } catch (NumberFormatException e) {
                throw new DecisionTreeParserException(
                        "Invalid default value \"" + value + "\" for integer result attribute \"" + name + "\"");
            }
        }

        return builder.build();
    }

    private Node parseTable(final Element tableElement, final Map<String, InputType> types,
            final ResultSpec resultSpec) throws DecisionTreeParserException {
        if (tableElement == null) {
            throw new DecisionTreeParserException("Invalid XML: No element named \"tree\" found");
        }

        final Node rootNode = parseInputs("/", tableElement.getChildren(), types, resultSpec);
        rootNode.validate();
        return rootNode;
    }

    private Node parseInputs(final String parentPath, final List<Element> inputs,
            final Map<String, InputType> types, final ResultSpec resultSpec) throws DecisionTreeParserException {
        final String inputName = getInputName(parentPath, inputs);
        final String nodePath = parentPath + inputName;
        final InputType inputType = getInputType(inputName, types);
        final NodeBuilder builder = inputType.createNodeBuilder();

        for (Element input : inputs) {
            final String value = input.getAttributeValue("value");
            final String refId = input.getAttributeValue("refid");
            final String childPath = nodePath + "=" + value;
            final List<Element> children = input.getChildren();

            if (refId != null) {
                if (!children.isEmpty()) {
                    throw new DecisionTreeParserException(
                            "Node at path " + childPath + " may not have both a refid and child elements.");
                } else {
                    builder.addReferenceMapping(value, refId);
                }
            } else if (children.isEmpty()) {
                throw new DecisionTreeParserException(
                        "Node at path " + childPath + " must have a result, child inputs, or a refid");
            } else if (children.size() == 1 && "result".equals(children.get(0).getName())) {
                builder.addResultMapping(value, parseResult(children.get(0), resultSpec));
            } else {
                builder.addNodeMapping(value, parseInputs(childPath + "/", children, types, resultSpec));
            }
        }

        return builder.build();
    }

    /**
     * Creates a result specification for this node. Does not create the actual result object,
     * since there's no guarantee that that object will be immutable.
     */
    private ResultNode parseResult(final Element element, final ResultSpec resultSpec)
            throws DecisionTreeParserException {
        final ResultNode.Builder builder = new ResultNode.Builder(resultSpec);
        for (final Attribute attribute : element.getAttributes()) {
            final String name = attribute.getName();
            final String value = attribute.getValue();
            builder.addAttribute(name, value);
        }

        return builder.build();
    }

    private InputType getInputType(final String name, final Map<String, InputType> types)
            throws DecisionTreeParserException {
        final InputType inputType = types.get(name);
        if (inputType == null) {
            throw new DecisionTreeParserException("Undefined input name: \"" + name + "\"");
        }

        return inputType;
    }

    private String getInputName(final String path, final List<Element> children)
            throws DecisionTreeParserException {
        if (children.isEmpty())
            throw new DecisionTreeParserException("Node at path " + path + " has no children");

        final Set<String> inputTypes = new HashSet<String>();
        for (Element element : children) {
            inputTypes.add(element.getName());
        }

        if (inputTypes.size() != 1) {
            throw new DecisionTreeParserException("Node at path " + path + " must have only one child type.");
        } else {
            return inputTypes.iterator().next();
        }
    }

    @Override
    public DecisionTree parse(final Reader inputReader) throws DecisionTreeParserException {
        try {
            final Document doc = new SAXBuilder().build(inputReader);
            return parseDocument(doc);
        } catch (JDOMException e) {
            throw new DecisionTreeParserException(e);
        } catch (IOException e) {
            throw new DecisionTreeParserException(e);
        }
    }

    @Override
    public DecisionTree parse(final InputStream inputStream) throws DecisionTreeParserException {
        try {
            final Document doc = new SAXBuilder().build(inputStream);
            return parseDocument(doc);
        } catch (JDOMException e) {
            throw new DecisionTreeParserException(e);
        } catch (IOException e) {
            throw new DecisionTreeParserException(e);
        }
    }

    int parseMinInt(final String min, final String fieldName) throws DecisionTreeParserException {
        if ("unbounded".equals(min))
            return Integer.MIN_VALUE;
        try {
            return Integer.parseInt(min);
        } catch (NumberFormatException e) {
            throw new DecisionTreeParserException(
                    "Invalid min value for field " + fieldName + ". Value = \"" + min + "\"");
        }
    }

    int parseMaxInt(final String max, final String fieldName) throws DecisionTreeParserException {
        if ("unbounded".equals(max))
            return Integer.MAX_VALUE;
        try {
            return Integer.parseInt(max);
        } catch (NumberFormatException e) {
            throw new DecisionTreeParserException(
                    "Invalid max value for field " + fieldName + ". Value = \"" + max + "\"");
        }
    }

    protected Map<String, InputType> parseInputTypes(final Element child) throws DecisionTreeParserException {
        final Map<String, InputType> types = new HashMap<String, InputType>(child.getChildren().size());

        for (Element typeElement : child.getChildren()) {
            final InputType type = parseInputType(typeElement);

            if ("result".equals(type.getName())) {
                throw new DecisionTreeParserException(
                        "Invalid configuration: No input-type may be named \"result\" since it is() a reserved keyword");
            }

            types.put(type.getName(), type);
        }

        return types;
    }

    private InputType parseInputType(final Element typeElement) throws DecisionTreeParserException {
        final String typeName = typeElement.getName();
        if ("string-type".equals(typeName)) {
            return parseStringInputType(typeElement);
        } else if ("integer-type".equals(typeName)) {
            return parseIntegerInputType(typeElement);
        } else if ("boolean-type".equals(typeName)) {
            return parseBooleanInputType(typeElement);
        } else {
            throw new DecisionTreeParserException("Unknown input-type: " + typeName);
        }
    }

    InputType parseBooleanInputType(final Element typeElement) {
        final String name = typeElement.getAttributeValue("name");
        final BooleanInputType.Builder builder = new BooleanInputType.Builder(name);
        return builder.build();
    }

    private InputType parseIntegerInputType(final Element typeElement) throws DecisionTreeParserException {
        final String name = typeElement.getAttributeValue("name");
        final IntegerInputType.Builder builder = new IntegerInputType.Builder(name);
        builder.setMinValue(parseMinInt(typeElement.getAttributeValue("min"), "integer-type " + name));
        builder.setMaxValue(parseMaxInt(typeElement.getAttributeValue("max"), "integer-type " + name));
        return builder.build();
    }

    InputType parseStringInputType(final Element typeElement) throws DecisionTreeParserException {
        final String name = typeElement.getAttributeValue("name");
        final StringInputType.Builder builder = new StringInputType.Builder(name);
        boolean hasDefaultValue = false;
        for (Element child : typeElement.getChildren("value")) {
            final boolean isDefault = "true".equals(child.getAttributeValue("default"));
            if (isDefault && hasDefaultValue) {
                throw new DecisionTreeParserException(
                        "Input-type \"" + name + "\" may not have more than one default type.");
            }
            builder.addEnumValue(child.getTextNormalize(), isDefault);
            hasDefaultValue = isDefault;
        }
        return builder.build();
    }
}