talkeeg.bf.schema.SchemaSource.java Source code

Java tutorial

Introduction

Here is the source code for talkeeg.bf.schema.SchemaSource.java

Source

/*
 * Copyright (c) 2014, wayerr (radiofun@ya.ru).
 *
 *      This file is part of talkeeg-parent.
 *
 *      talkeeg-parent 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.
 *
 *      talkeeg-parent 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 talkeeg-parent.  If not, see <http://www.gnu.org/licenses/>.
 */

package talkeeg.bf.schema;

import com.google.common.collect.ImmutableMap;
import com.google.common.io.ByteSource;
import com.google.common.io.Resources;
import org.w3c.dom.*;
import org.xml.sax.*;
import talkeeg.bf.EntryType;
import talkeeg.bf.MetaTypes;
import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.validation.SchemaFactory;
import java.io.InputStream;
import java.net.URL;
import java.nio.ByteOrder;
import java.util.Map;
import java.util.TreeMap;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * tool for loading schema <p/>
 *
 * Created by wayerr on 21.11.14.
 */
public final class SchemaSource {

    private static final Logger log = Logger.getLogger(SchemaSource.class.getName());
    public static final ErrorHandler ERROR_HANDLER = new ErrorHandler() {

        @Override
        public void warning(SAXParseException exception) throws SAXException {
            log.log(Level.WARNING, "", exception);
        }

        @Override
        public void error(SAXParseException exception) throws SAXException {
            throw exception;
        }

        @Override
        public void fatalError(SAXParseException exception) throws SAXException {
            throw exception;
        }
    };
    public static final String ATTR_FIELD_NAME = "fieldName";
    public static final String ATTR_STORE_AS = "storeAs";

    private static final class LoadContext {
        private final Map<Integer, Struct.Builder> structMap = new TreeMap<>();

        private Struct.Builder getStruct(int structId) {
            final Struct.Builder struct = structMap.get(structId);
            if (struct == null) {
                throw new RuntimeException("can not find previously defined struct with id: " + structId);
            }
            return struct;
        }

        private void addStruct(Struct.Builder struct) {
            final int structId = struct.getId();
            final Struct.Builder oldStruct = structMap.put(structId, struct);
            if (oldStruct != null) {
                throw new RuntimeException("find two struct with equal id: " + structId);
            }
        }
    }

    private static final String NS = "http://talkeeg/ns/tgbf";

    public static final String NAME_MESSAGE = "message";
    public static final String NAME_STRUCT = "struct";
    public static final String NAME_PRIMITIVE = "primitive";
    public static final String NAME_LIST = "list";
    public static final String NAME_MAP = "map";
    public static final String NAME_UNION = "union";
    public static final String NAME_GENERIC = "generic";
    public static final String NAME_INTEGER = MetaTypes.INTEGER;
    public static final String NAME_BOOLEAN = MetaTypes.BOOLEAN;
    public static final String NAME_FLOAT = MetaTypes.FLOAT;
    public static final String NAME_DATETIME = MetaTypes.DATETIME;
    public static final String NAME_BLOB = MetaTypes.BLOB;
    public static final String NAME_STRING = MetaTypes.STRING;
    public static final String NAME_ID = MetaTypes.ID;
    public static final Map<String, EntryType> ENTRY_TYPE_MAP = ImmutableMap.<String, EntryType>builder()
            .put("NULL", EntryType.NULL).put("HALF", EntryType.HALF).put("01_BYTE", EntryType.BYTE_1)
            .put("02_BYTE", EntryType.BYTE_2).put("04_BYTE", EntryType.BYTE_4).put("08_BYTE", EntryType.BYTE_8)
            .put("16_BYTE", EntryType.BYTE_16).put("BLOB", EntryType.BYTES).put("STRUCT", EntryType.STRUCT)
            .put("LIST", EntryType.LIST).build();

    public static Schema fromResource(String resourceUri) throws Exception {
        final URL url = Resources.getResource(resourceUri);
        final ByteSource bs = Resources.asByteSource(url);
        try (InputStream is = bs.openStream()) {
            return fromInputStream(is);
        }
    }

    protected static Schema fromInputStream(InputStream is) throws Exception {

        final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        factory.setValidating(false);
        factory.setNamespaceAware(true);
        try {
            final SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
            final javax.xml.validation.Schema xmlschema = schemaFactory
                    .newSchema(Resources.getResource("tgbf.xsd"));
            factory.setSchema(xmlschema);
        } catch (java.lang.IllegalArgumentException e) {
            // android does not contains SchemaFactory
            // see https://code.google.com/p/android/issues/detail?id=7395
        }

        final DocumentBuilder documentBuilder = factory.newDocumentBuilder();
        documentBuilder.setErrorHandler(ERROR_HANDLER);

        final Document doc = documentBuilder.parse(is);

        final Schema.Builder builder = Schema.builder();
        final Element rootElement = doc.getDocumentElement();

        loadByteOrder(builder, rootElement);

        final LoadContext context = new LoadContext();
        final NodeList messages = rootElement.getChildNodes();
        final int length = messages.getLength();
        for (int i = 0; i < length; ++i) {
            final Node node = messages.item(i);
            if (node.getNodeType() != Node.ELEMENT_NODE) {
                continue;
            }
            final String name = node.getLocalName();
            if (!NAME_MESSAGE.equals(name) && !NAME_STRUCT.equals(name)) {
                throw new RuntimeException("expected 'message' or 'struct' node, but found " + name);
            }
            final Struct.Builder structBuilder = loadStruct(context, node);
            final Struct struct = structBuilder.build();
            builder.putMessage(struct);
        }

        return builder.build();
    }

    private static void loadByteOrder(Schema.Builder builder, Element rootElement) {
        final String byteOrderName = getAttributeValue(rootElement.getAttributes(), "byteOrder", true);
        builder.setByteOrder("BIG_ENDIAN".equals(byteOrderName) ? ByteOrder.BIG_ENDIAN : ByteOrder.LITTLE_ENDIAN);
    }

    private static EntryType getEntryType(NamedNodeMap attributes, String attrName) {
        final String stringValue = getAttributeValue(attributes, attrName, false);
        if (stringValue == null) {
            return null;
        }
        EntryType entryType = ENTRY_TYPE_MAP.get(stringValue);
        if (entryType == null) {
            throw new RuntimeException("Unknown entry type :" + stringValue + " known: " + ENTRY_TYPE_MAP.keySet());
        }
        return entryType;
    }

    private static Struct.Builder loadStruct(LoadContext context, Node node) {
        final NamedNodeMap attrs = node.getAttributes();
        final int structId = Integer.parseInt(getAttributeValue(attrs, "structId", true));
        final NodeList fields = node.getChildNodes();
        final int length = fields.getLength();
        if (length == 0) {
            //empty struct interpreted as reference to a previously defined struct
            return context.getStruct(structId);
        }
        Struct.Builder b = Struct.builder();
        b.setId(structId);
        for (int i = 0; i < length; ++i) {
            final Node field = fields.item(i);
            final SchemaEntry entry = loadEntry(context, field);
            if (entry != null) {
                b.addField(entry);
            }
        }
        context.addStruct(b);
        return b;
    }

    private static SchemaEntryBuilder loadPrimitive(Node node, Class<?> clazz) {
        return loadPrimitive(node, clazz, null);
    }

    private static SchemaEntryBuilder loadPrimitive(Node node, Class<?> clazz, final EntryType type) {
        final PrimitiveEntry.Builder builder = PrimitiveEntry.builder();
        final NamedNodeMap attributes = node.getAttributes();
        EntryType entryType = type;
        if (entryType == null) {
            entryType = getEntryType(attributes, ATTR_STORE_AS);
            if (entryType == null) {
                throw new RuntimeException("Need specify 'storeAs' attribute in " + node);
            }
        }
        builder.setType(entryType);
        builder.setJavaType(clazz);
        builder.setMetaType(node.getLocalName());
        return builder;
    }

    private static ListEntry.Builder loadList(LoadContext context, Node node) {
        final ListEntry.Builder b = new ListEntry.Builder();
        final NodeList childNodes = node.getChildNodes();
        final int length = childNodes.getLength();
        SchemaEntry itemEntry = null;
        for (int i = 0; i < length; ++i) {
            Node item = childNodes.item(i);
            itemEntry = loadEntry(context, item);
            if (itemEntry != null) {
                break;
            }
        }
        b.setItemEntry(itemEntry);
        return b;
    }

    private static MapEntry.Builder loadMap(LoadContext context, Node node) {
        final MapEntry.Builder b = new MapEntry.Builder();
        final NodeList childNodes = node.getChildNodes();
        final int length = childNodes.getLength();
        int attrIndex = 0;
        for (int i = 0; i < length; ++i) {
            Node item = childNodes.item(i);
            SchemaEntry itemEntry = loadEntry(context, item);
            if (itemEntry != null) {
                if (attrIndex == 0) {
                    b.setKeyEntry(itemEntry);
                } else if (attrIndex == 1) {
                    b.setValueEntry(itemEntry);
                } else {
                    throw new RuntimeException("to many entries in map node: " + attrIndex);
                }
                attrIndex++;
            }
        }
        return b;
    }

    private static SchemaEntry loadEntry(LoadContext context, Node node) {
        if (node.getNodeType() != Node.ELEMENT_NODE) {
            return null;
        }
        final String name = node.getLocalName();
        final SchemaEntryBuilder entry;
        switch (name) {
        case NAME_MESSAGE:
        case NAME_STRUCT:
            entry = loadStruct(context, node);
            break;
        case NAME_DATETIME:
            entry = loadPrimitive(node, Long.class, EntryType.BYTE_8);
            break;
        case NAME_FLOAT:
            entry = loadPrimitive(node, Double.class);
            break;
        case NAME_INTEGER:
            entry = loadPrimitive(node, Long.class);
            break;
        case NAME_BOOLEAN:
            entry = loadPrimitive(node, Boolean.class, EntryType.HALF);
            break;
        case NAME_PRIMITIVE:
            entry = loadPrimitive(node, Object.class);
            break;
        case NAME_ID:
            entry = loadPrimitive(node, byte[].class, EntryType.BYTE_16);
            break;
        case NAME_BLOB:
            entry = loadPrimitive(node, byte[].class, EntryType.BYTES);
            break;
        case NAME_STRING:
            entry = loadPrimitive(node, String.class, EntryType.BYTES);
            break;
        case NAME_GENERIC:
            entry = loadGeneric(node);
            break;
        case NAME_LIST:
            entry = loadList(context, node);
            break;
        case NAME_MAP:
            entry = loadMap(context, node);
            break;
        case NAME_UNION:
            entry = loadUnion(context, node);
            break;
        default:
            throw new RuntimeException("unsupported node type: " + name);
        }

        boolean fieldNameRequired = false;
        final Node parentNode = node.getParentNode();
        if (parentNode != null) {
            final String localName = parentNode.getLocalName();
            fieldNameRequired = localName.equals(NAME_MESSAGE) || localName.equals(NAME_STRUCT);
        }
        entry.setFieldName(getAttributeValue(node.getAttributes(), ATTR_FIELD_NAME, fieldNameRequired));
        return entry.build();
    }

    private static GenericEntry.Builder loadGeneric(Node node) {
        return GenericEntry.builder();
    }

    private static UnionEntry.Builder loadUnion(LoadContext context, Node node) {
        UnionEntry.Builder b = UnionEntry.builder();
        final NodeList fields = node.getChildNodes();
        final int length = fields.getLength();
        for (int i = 0; i < length; ++i) {
            final Node field = fields.item(i);
            if (field.getNodeType() != Node.ELEMENT_NODE) {
                continue;
            }
            final SchemaEntry entry = loadEntry(context, field);
            if (entry != null) {
                b.addEntry(entry);
            }
        }
        return b;
    }

    private static String getAttributeValue(NamedNodeMap attrs, String name, boolean required) {
        final Node node = attrs.getNamedItemNS(null, name);
        if (node == null) {
            if (required) {
                //this situation is possible when the validation is disabled or error in the description of the scheme
                throw new RuntimeException("need attribute: " + name);
            } else {
                return null;
            }
        }
        return node.getNodeValue();
    }
}