de.pixida.logtest.automatondefinitions.JsonAutomatonDefinition.java Source code

Java tutorial

Introduction

Here is the source code for de.pixida.logtest.automatondefinitions.JsonAutomatonDefinition.java

Source

/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 *
 * Copyright (c) 2016 Pixida GmbH
 */

package de.pixida.logtest.automatondefinitions;

import java.io.File;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;

import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.Validate;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import de.pixida.logtest.logreaders.ILogEntry;

public class JsonAutomatonDefinition implements IAutomatonDefinition {
    public static final Charset EXPECTED_CHARSET = StandardCharsets.UTF_8;

    private static final String CONFIGURATION_KEY_DEPRECATED = "Use of configuration key '{}' is deprecated! Use '{}' instead.";

    private static final Logger LOG = LoggerFactory.getLogger(JsonAutomatonDefinition.class);

    private final File jsonFile;
    private final String jsonData;

    private boolean automatonLoaded = false;
    private final List<GenericNode> nodes = new ArrayList<>();
    private final List<GenericEdge> edges = new ArrayList<>();
    private final Map<String, GenericNode> mapIdNode = new HashMap<>();
    private final String name;
    private String description;
    private String onLoad;
    private String scriptLanguage;

    public JsonAutomatonDefinition(final File aJsonFile) {
        Validate.notNull(aJsonFile);
        this.jsonFile = aJsonFile;
        this.name = this.jsonFile.getName();
        this.jsonData = null;
        LOG.debug("Constructed loader from JSON file: {}", this.jsonFile.getAbsolutePath());
    }

    public JsonAutomatonDefinition(final String aName, final String data) {
        Validate.notNull(aName);
        Validate.notNull(data);
        this.jsonFile = null;
        this.name = aName;
        this.jsonData = data;
        LOG.debug("Constructed loader of automaton '{}' from data '{}' (length: '{}')", this.name, this.jsonData,
                this.jsonData.length());
    }

    @Override
    public List<INodeDefinition> getNodes() {
        return new ArrayList<>(this.nodes);
    }

    @Override
    public List<IEdgeDefinition> getEdges() {
        return new ArrayList<>(this.edges);
    }

    @Override
    public String getOnLoad() {
        return this.onLoad;
    }

    @Override
    public String getDisplayName() {
        return this.jsonFile.getName();
    }

    @Override
    public String getDescription() {
        return this.description;
    }

    @Override
    public String getScriptLanguage() {
        return this.scriptLanguage;
    }

    @Override
    public void load() {
        if (!this.automatonLoaded) {
            try {
                final String data = this.jsonData == null
                        ? FileUtils.readFileToString(this.jsonFile, EXPECTED_CHARSET)
                        : this.jsonData;
                final JSONObject root = new JSONObject(data);
                this.description = this.loadOptStringAttributeFromJsonObjectWithDeprecatedAlternative(root,
                        JsonKey.ROOT_DESCRIPTION, JsonKey.ROOT_DESCRIPTION_DEPRECATED);
                this.onLoad = this.loadOptStringAttributeFromJsonObject(root, JsonKey.ROOT_ONLOAD);
                this.scriptLanguage = this.loadOptStringAttributeFromJsonObject(root, JsonKey.ROOT_SCRIPT_LANGUAGE);
                this.extractNodesAndEdges(root);
                this.automatonLoaded = true;
            } catch (final IOException ioe) {
                throw new AutomatonLoadingException("Failed to read json file", ioe);
            } catch (final JSONException jsonEx) {
                throw new AutomatonLoadingException(
                        "Json data malformed (invalid syntax, missing attributes or wrong attribute types)",
                        jsonEx);
            }
        }
    }

    private void extractNodesAndEdges(final JSONObject root) {
        this.nodes.clear();
        this.edges.clear();
        this.mapIdNode.clear();
        final JSONArray jsonNodes = root.getJSONArray(JsonKey.ROOT_NODES.getKey());
        this.extractNodes(jsonNodes);
        this.extractEdges(jsonNodes);
    }

    private void extractNodes(final JSONArray jsonNodes) {
        for (int i = 0; i < jsonNodes.length(); i++) {
            final JSONObject node = jsonNodes.getJSONObject(i);
            final GenericNode newNode = new GenericNode(node.getString("id"));
            newNode.setName(this.loadOptStringAttributeFromJsonObject(node, JsonKey.NODE_NAME));
            newNode.setDescription(this.loadOptStringAttributeFromJsonObjectWithDeprecatedAlternative(node,
                    JsonKey.NODE_DESCRIPTION, JsonKey.NODE_DESCRIPTION_DEPRECATED));
            newNode.setType(
                    this.loadOptEnumAttributeFromJsonObject(node, INodeDefinition.Type.class, JsonKey.NODE_TYPE));
            if (newNode.getType() == null) {
                newNode.setType(this.readTypeDefinedByDeprecatedFlags(node));
            }
            newNode.setOnEnter(this.loadOptStringAttributeFromJsonObject(node, JsonKey.NODE_ON_ENTER));
            newNode.setOnLeave(this.loadOptStringAttributeFromJsonObject(node, JsonKey.NODE_ON_LEAVE));
            newNode.setSuccessCheckExp(
                    this.loadOptStringAttributeFromJsonObject(node, JsonKey.NODE_SUCCESS_CHECK_EXP));
            newNode.setWait(
                    node.has(JsonKey.NODE_WAIT.getKey()) ? node.getBoolean(JsonKey.NODE_WAIT.getKey()) : false);
            this.nodes.add(newNode);
            this.mapIdNode.put(newNode.getId(), newNode);
        }
    }

    private INodeDefinition.Type readTypeDefinedByDeprecatedFlags(final JSONObject node) {
        final Set<INodeDefinition.Flag> flags = new HashSet<>();
        this.readDeprecatedFlag(node, JsonKey.NODE_INITIAL, INodeDefinition.Flag.IS_INITIAL, flags);
        this.readDeprecatedFlag(node, JsonKey.NODE_FAILURE, INodeDefinition.Flag.IS_FAILURE, flags);
        this.readDeprecatedFlag(node, JsonKey.NODE_SUCCESS, INodeDefinition.Flag.IS_SUCCESS, flags);
        if (flags.size() > 1) {
            throw new AutomatonLoadingException("A node can have only one type. Use property '"
                    + JsonKey.NODE_TYPE.getKey() + "' instead of deprecated flag properties.");
        }
        if (flags.isEmpty()) {
            return null;
        }
        if (flags.contains(INodeDefinition.Flag.IS_INITIAL)) {
            return INodeDefinition.Type.INITIAL;
        } else if (flags.contains(INodeDefinition.Flag.IS_FAILURE)) {
            return INodeDefinition.Type.FAILURE;
        } else if (flags.contains(INodeDefinition.Flag.IS_SUCCESS)) {
            return INodeDefinition.Type.SUCCESS;
        }
        throw new RuntimeException("Internal error. No type for deprecated flag(s): " + flags.toString());
    }

    private void readDeprecatedFlag(final JSONObject node, final JsonKey key, final INodeDefinition.Flag mapToFlag,
            final Set<INodeDefinition.Flag> flags) {
        if (node.has(key.getKey())) {
            LOG.warn(CONFIGURATION_KEY_DEPRECATED, key.getKey(), JsonKey.NODE_TYPE.getKey());
            if (node.getBoolean(key.getKey())) {
                flags.add(mapToFlag);
            }
        }
    }

    private void extractEdges(final JSONArray jsonNodes) {
        for (int i = 0; i < jsonNodes.length(); i++) {
            final JSONObject node = jsonNodes.getJSONObject(i);
            if (node.has(JsonKey.NODE_OUTGOING_EDGES.getKey())) {
                final JSONArray outgoingEdges = node.getJSONArray(JsonKey.NODE_OUTGOING_EDGES.getKey());
                for (int j = 0; j < outgoingEdges.length(); j++) {
                    final JSONObject edge = outgoingEdges.getJSONObject(j);
                    final GenericNode source = this.mapIdNode.get(node.getString(JsonKey.NODE_ID.getKey()));
                    final String destinationNodeId = edge.getString(JsonKey.EDGE_DESTINATION.getKey());
                    final GenericNode destination = this.mapIdNode.get(destinationNodeId);
                    assert source != null;
                    if (destination == null) {
                        throw new AutomatonLoadingException("Destination node not found: " + destinationNodeId);
                    }
                    final GenericEdge newEdge = new GenericEdge(edge.getString(JsonKey.EDGE_ID.getKey()), source,
                            destination);
                    newEdge.setName(this.loadOptStringAttributeFromJsonObject(edge, JsonKey.EDGE_NAME));
                    newEdge.setDescription(this.loadOptStringAttributeFromJsonObjectWithDeprecatedAlternative(edge,
                            JsonKey.EDGE_DESCRIPTION, JsonKey.EDGE_DESCRIPTION_DEPRECATED));
                    newEdge.setOnWalk(this.loadOptStringAttributeFromJsonObject(edge, JsonKey.EDGE_ON_WALK));
                    this.retrieveEdgeConditions(edge, newEdge);
                    this.edges.add(newEdge);
                }
            }
        }
    }

    private void retrieveEdgeConditions(final JSONObject edge, final GenericEdge newEdge) {
        newEdge.setRegExp(this.loadOptStringAttributeFromJsonObject(edge, JsonKey.EDGE_REG_EXP));
        newEdge.setCheckExp(this.loadOptStringAttributeFromJsonObject(edge, JsonKey.EDGE_CHECK_EXP));
        newEdge.setTriggerAlways(this.loadOptBooleanAttributeFromJsonObject(edge, JsonKey.EDGE_TRIGGER_ALWAYS));
        newEdge.setTriggerOnEof(this.loadOptBooleanAttributeFromJsonObject(edge, JsonKey.EDGE_TRIGGER_ON_EOF));
        newEdge.setRequiredConditions(this.loadOptEnumAttributeFromJsonObject(edge,
                IEdgeDefinition.RequiredConditions.class, JsonKey.EDGE_REQUIRED_CONDITIONS));
        newEdge.setTimeIntervalSinceLastMicrotransition(
                this.parseTimeInterval(edge, JsonKey.EDGE_TIME_INTERVAL_SINCE_LAST_MICROTRANSITION));
        newEdge.setTimeIntervalSinceLastTransition(
                this.parseTimeInterval(edge, JsonKey.EDGE_TIME_INTERVAL_SINCE_LAST_TRANSITION));
        newEdge.setTimeIntervalSinceAutomatonStart(
                this.parseTimeInterval(edge, JsonKey.EDGE_TIME_INTERVAL_SINCE_AUTOMATON_START));
        newEdge.setTimeIntervalForEvent(this.parseTimeInterval(edge, JsonKey.EDGE_TIME_INTERVAL_FOR_EVENT));
        newEdge.setChannel(this.loadOptStringAttributeFromJsonObject(edge, JsonKey.EDGE_CHANNEL));
        if (newEdge.getChannel() == null) {
            newEdge.setChannel(ILogEntry.DEFAULT_CHANNEL);
        }
    }

    private ITimeInterval parseTimeInterval(final JSONObject edge, final JsonKey key) {
        if (edge.has(key.getKey())) {
            final JSONObject timeIntervalObject = edge.optJSONObject(key.getKey());
            if (timeIntervalObject != null) {
                final GenericTimeInterval timeInterval = new GenericTimeInterval();
                timeInterval.setMin(this.parseDuration(timeIntervalObject, JsonKey.TIME_INTERVAL_MIN));
                timeInterval.setMax(this.parseDuration(timeIntervalObject, JsonKey.TIME_INTERVAL_MAX));
                if (timeInterval.getMin() == null && timeInterval.getMax() == null) {
                    return null;
                } else {
                    return timeInterval;
                }
            }

            // Dependency to automaton to JSON converter: Reading the time interval as a string only plays a role if the user typed the
            // JSON automaton definition by hand (unless the JSON converter supports this). It's not clear if this feature shall be
            // supported in future.
            // Above, we fetch "opt" JSONObject; when removing the following code, we can strictly assume it "is" a JSON object.
            final String timeIntervalString = edge.optString(key.getKey());
            if (timeIntervalString != null) {
                return StringToGenericTimeIntervalConverter.convertStringToTimestamp(timeIntervalString);
            }
        }

        return null;
    }

    private GenericDuration parseDuration(final JSONObject timeIntervalObject, final JsonKey key) {
        final JSONObject durationObject = timeIntervalObject.optJSONObject(key.getKey());
        if (durationObject == null) {
            return null;
        } else {
            final GenericDuration d = new GenericDuration();
            d.setInclusive(durationObject.getBoolean(JsonKey.TIME_INTERVAL_DURATION_IS_INCLUSIVE.getKey()));
            d.setValue(durationObject.getString(JsonKey.TIME_INTERVAL_DURATION_VALUE.getKey()));
            d.setUnit(StringToGenericTimeIntervalConverter
                    .parseUnit(durationObject.getString(JsonKey.TIME_INTERVAL_DURATION_UNIT.getKey())));
            return d;
        }
    }

    private String loadOptStringAttributeFromJsonObject(final JSONObject jsonObject, final JsonKey key) {
        return jsonObject.has(key.getKey()) ? jsonObject.getString(key.getKey()) : null;
    }

    private Boolean loadOptBooleanAttributeFromJsonObject(final JSONObject jsonObject, final JsonKey key) {
        return jsonObject.has(key.getKey()) ? jsonObject.getBoolean(key.getKey()) : null;
    }

    private String loadOptStringAttributeFromJsonObjectWithDeprecatedAlternative(final JSONObject jsonObject,
            final JsonKey key, final JsonKey oldKey) {
        String value = this.loadOptStringAttributeFromJsonObject(jsonObject, key);
        if (value == null) {
            value = this.loadOptStringAttributeFromJsonObject(jsonObject, oldKey);
            if (value != null) {
                LOG.warn(CONFIGURATION_KEY_DEPRECATED, key.getKey(), oldKey.getKey());
            }
        }
        return value;
    }

    private <E extends Enum<E>> E loadOptEnumAttributeFromJsonObject(final JSONObject jsonObject,
            final Class<E> enumClz, final JsonKey key) {
        final String value = this.loadOptStringAttributeFromJsonObject(jsonObject, key);
        if (value == null) {
            return null;
        }
        final String valueLc = value.toLowerCase(Locale.US);
        return Arrays.stream(enumClz.getEnumConstants())
                .filter(enumConstant -> valueLc.equals(enumConstant.toString().toLowerCase(Locale.US))).findFirst()
                .orElseThrow(
                        () -> new AutomatonLoadingException("Invalid value for required conditions: " + value));
    }

    // Just for logging output / no business use
    @Override
    public String toString() {
        return this.name;
    }
}