All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.esotericsoftware.yamlbeans.YamlReader Maven / Gradle / Ivy

/*
 * Copyright (c) 2008 Nathan Sweet
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation
 * files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy,
 * modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software
 * is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
 * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
 * IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

package com.esotericsoftware.yamlbeans;

import static com.esotericsoftware.yamlbeans.parser.EventType.*;

import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Map.Entry;

import com.esotericsoftware.yamlbeans.Beans.Property;
import com.esotericsoftware.yamlbeans.parser.AliasEvent;
import com.esotericsoftware.yamlbeans.parser.CollectionStartEvent;
import com.esotericsoftware.yamlbeans.parser.Event;
import com.esotericsoftware.yamlbeans.parser.Parser;
import com.esotericsoftware.yamlbeans.parser.Parser.ParserException;
import com.esotericsoftware.yamlbeans.parser.ScalarEvent;
import com.esotericsoftware.yamlbeans.scalar.ScalarSerializer;
import com.esotericsoftware.yamlbeans.tokenizer.Tokenizer.TokenizerException;
import zoomba.lang.core.types.ZNumber;
import zoomba.lang.core.types.ZTypes;

/**
 * Deserializes Java objects from YAML.
 *
 * @author Nathan Sweet
 */
public class YamlReader {
    private final YamlConfig config;
    Parser parser;
    private final Map anchors = new HashMap();

    public YamlReader(Reader reader) {
        this(reader, new YamlConfig());
    }

    public YamlReader(Reader reader, YamlConfig config) {
        this.config = config;
        parser = new Parser(reader, config.readConfig.defaultVersion);
    }

    public YamlReader(String yaml) {
        this(new StringReader(yaml));
    }

    public YamlReader(String yaml, YamlConfig config) {
        this(new StringReader(yaml), config);
    }

    public YamlConfig getConfig() {
        return config;
    }

    /**
     * Return the object with the given alias, or null. This is only valid after objects have been read and before
     * {@link #close()}
     */
    public Object get(String alias) {
        return anchors.get(alias);
    }

    public void close() throws IOException {
        parser.close();
        anchors.clear();
    }

    /**
     * Reads the next YAML document and deserializes it into an object. The type of object is defined by the YAML tag. If there is
     * no YAML tag, the object will be an {@link ArrayList}, {@link HashMap}, or String.
     */
    public Object read() throws YamlException {
        return read(null);
    }

    /**
     * Reads an object of the specified type from YAML.
     *
     * @param type The type of object to read. If null, behaves the same as {{@link #read()}.
     */
    public  T read(Class type) throws YamlException {
        return read(type, null);
    }

    /**
     * Reads an array, Map, List, or Collection object of the specified type from YAML, using the specified element type.
     *
     * @param type The type of object to read. If null, behaves the same as {{@link #read()}.
     */
    public  T read(Class type, Class elementType) throws YamlException {
        try {
            while (true) {
                Event event = parser.getNextEvent();
                if (event == null) return null;
                if (event.type == STREAM_END) return null;
                if (event.type == DOCUMENT_START) break;
            }
            return (T) readValue(type, elementType, null);
        } catch (ParserException ex) {
            throw new YamlException("Error parsing YAML.", ex);
        } catch (TokenizerException ex) {
            throw new YamlException("Error tokenizing YAML.", ex);
        }
    }

    /**
     * Reads an object from the YAML. Can be overidden to take some action for any of the objects returned.
     */
    protected Object readValue(Class type, Class elementType, Class defaultType)
            throws YamlException, ParserException, TokenizerException {
        String tag = null, anchor = null;
        Event event = parser.peekNextEvent();

        switch (event.type) {
            case ALIAS:
                parser.getNextEvent();
                anchor = ((AliasEvent) event).anchor;
                Object value = anchors.get(anchor);
                if (value == null) throw new YamlReaderException("Unknown anchor: " + anchor);
                return value;
            case MAPPING_START:
            case SEQUENCE_START:
                tag = ((CollectionStartEvent) event).tag;
                anchor = ((CollectionStartEvent) event).anchor;
                break;
            case SCALAR:
                tag = ((ScalarEvent) event).tag;
                anchor = ((ScalarEvent) event).anchor;
                break;
            default:
        }

        return readValueInternal(this.chooseType(tag, defaultType, type), elementType, anchor);
    }

    private Class chooseType(String tag, Class defaultType, Class providedType) throws YamlReaderException {
        if (tag != null && config.readConfig.classTags) {
            Class userConfiguredByTag = config.tagToClass.get(tag);
            if (userConfiguredByTag != null) {
                return userConfiguredByTag;
            }

            ClassLoader classLoader = (config.readConfig.classLoader == null ? this.getClass().getClassLoader()
                    : config.readConfig.classLoader);

            try {
                Class loadedFromTag = findTagClass(tag, classLoader);
                if (loadedFromTag != null) {
                    return loadedFromTag;
                }
            } catch (ClassNotFoundException e) {
                throw new YamlReaderException("Unable to find class specified by tag: " + tag);
            }
        }

        if (defaultType != null) {
            return defaultType;
        }

        // This may be null.
        return providedType;
    }

    /**
     * Used during reading when a tag is present, and {@link YamlConfig#setClassTag(String, Class)} was not used for that tag.
     * Attempts to load the class corresponding to that tag.
     * 

* If this returns a non-null Class, that will be used as the deserialization type regardless of whether a type was explicitly * asked for or if a default type exists. *

* If this returns null, no guidance will be provided by the tag and we will fall back to the default type or a requested * target type, if any exist. *

* If this throws a ClassNotFoundException, parsing will fail. *

* The default implementation is simply * *

     *  {@code Class.forName(tag, true, classLoader);}
     * 
*

* and never returns null. *

* You can override this to handle cases where you do not want to respect the type tags found in a document - e.g., if they * were output by another program using classes that do not exist on your classpath. */ protected Class findTagClass(String tag, ClassLoader classLoader) throws ClassNotFoundException { return Class.forName(tag, true, classLoader); } private Object readValueInternal(Class type, Class elementType, String anchor) throws YamlException, ParserException, TokenizerException { if (type == null || type == Object.class) { Event event = parser.peekNextEvent(); switch (event.type) { case MAPPING_START: type = LinkedHashMap.class; break; case SCALAR: final String value = ((ScalarEvent) event).value; if (((ScalarEvent) event).plain && value != null) { // automatic parsing boolean ? Boolean bV = ZTypes.bool(value); if (bV != null) { if (anchor != null) anchors.put(anchor, bV); parser.getNextEvent(); return bV; } // automatic support of shrinking and expanding ... when needed Number convertedValue = ZNumber.number(value); if (convertedValue != null) { if (anchor != null) anchors.put(anchor, convertedValue); parser.getNextEvent(); return convertedValue; } } type = String.class; break; case SEQUENCE_START: type = ArrayList.class; break; default: throw new YamlReaderException("Expected scalar, sequence, or mapping but found: " + event.type); } } if (type == String.class) { Event event = parser.getNextEvent(); if (event.type != SCALAR) throw new YamlReaderException("Expected scalar for String type but found: " + event.type); String value = ((ScalarEvent) event).value; if (anchor != null) anchors.put(anchor, value); return value; } if (Beans.isScalar(type)) { Event event = parser.getNextEvent(); if (event.type != SCALAR) throw new YamlReaderException( "Expected scalar for primitive type '" + type.getClass() + "' but found: " + event.type); String value = ((ScalarEvent) event).value; try { Object convertedValue; if (type == String.class) { convertedValue = value; } else if (type == Integer.TYPE) { convertedValue = value.length() == 0 ? 0 : Integer.decode(value); } else if (type == Integer.class) { convertedValue = value.length() == 0 ? null : Integer.decode(value); } else if (type == Boolean.TYPE) { convertedValue = value.length() == 0 ? false : Boolean.valueOf(value); } else if (type == Boolean.class) { convertedValue = value.length() == 0 ? null : Boolean.valueOf(value); } else if (type == Float.TYPE) { convertedValue = value.length() == 0 ? 0 : Float.valueOf(value); } else if (type == Float.class) { convertedValue = value.length() == 0 ? null : Float.valueOf(value); } else if (type == Double.TYPE) { convertedValue = value.length() == 0 ? 0 : Double.valueOf(value); } else if (type == Double.class) { convertedValue = value.length() == 0 ? null : Double.valueOf(value); } else if (type == Long.TYPE) { convertedValue = value.length() == 0 ? 0 : Long.decode(value); } else if (type == Long.class) { convertedValue = value.length() == 0 ? null : Long.decode(value); } else if (type == Short.TYPE) { convertedValue = value.length() == 0 ? 0 : Short.decode(value); } else if (type == Short.class) { convertedValue = value.length() == 0 ? null : Short.decode(value); } else if (type == Character.TYPE) { convertedValue = value.length() == 0 ? 0 : value.charAt(0); } else if (type == Character.class) { convertedValue = value.length() == 0 ? null : value.charAt(0); } else if (type == Byte.TYPE) { convertedValue = value.length() == 0 ? 0 : Byte.decode(value); } else if (type == Byte.class) { convertedValue = value.length() == 0 ? null : Byte.decode(value); } else throw new YamlException("Unknown field type."); if (anchor != null) anchors.put(anchor, convertedValue); return convertedValue; } catch (Exception ex) { throw new YamlReaderException("Unable to convert value to required type \"" + type + "\": " + value, ex); } } if (Enum.class.isAssignableFrom(type)) { Event event = parser.getNextEvent(); if (event.type != SCALAR) throw new YamlReaderException("Expected scalar for enum type but found: " + event.type); String enumValueName = ((ScalarEvent) event).value; if (enumValueName.length() == 0) return null; try { return Enum.valueOf(type, enumValueName); } catch (Exception ex) { throw new YamlReaderException("Unable to find enum value '" + enumValueName + "' for enum class: " + type.getName()); } } for (Entry entry : config.scalarSerializers.entrySet()) { if (entry.getKey().isAssignableFrom(type)) { ScalarSerializer serializer = entry.getValue(); Event event = parser.getNextEvent(); if (event.type != SCALAR) throw new YamlReaderException("Expected scalar for type '" + type + "' to be deserialized by scalar serializer '" + serializer.getClass().getName() + "' but found: " + event.type); Object value = serializer.read(((ScalarEvent) event).value); if (anchor != null) anchors.put(anchor, value); return value; } } Event event = parser.peekNextEvent(); switch (event.type) { case MAPPING_START: { // Must be a map or an object. event = parser.getNextEvent(); Object object; try { object = createObject(type); } catch (InvocationTargetException ex) { throw new YamlReaderException("Error creating object.", ex); } if (anchor != null) anchors.put(anchor, object); ArrayList keys = new ArrayList(); while (true) { if (parser.peekNextEvent().type == MAPPING_END) { parser.getNextEvent(); break; } Object key = readValue(null, null, null); // Explicit key/value pairs (using "? key\n: value\n") will come back as a map. boolean isExplicitKey = key instanceof Map; Object value = null; if (isExplicitKey) { Entry nameValuePair = (Entry) ((Map) key).entrySet().iterator().next(); key = nameValuePair.getKey(); value = nameValuePair.getValue(); } if (object instanceof Map) { // Add to map. if (config.tagSuffix != null) { Event nextEvent = parser.peekNextEvent(); switch (nextEvent.type) { case MAPPING_START: case SEQUENCE_START: ((Map) object).put(key + config.tagSuffix, ((CollectionStartEvent) nextEvent).tag); break; case SCALAR: ((Map) object).put(key + config.tagSuffix, ((ScalarEvent) nextEvent).tag); break; } } if (!isExplicitKey) value = readValue(elementType, null, null); if (!config.allowDuplicates && ((Map) object).containsKey(key)) { throw new YamlReaderException("Duplicate key found '" + key + "'"); } if (config.readConfig.autoMerge && "<<".equals(key) && value != null) mergeMap((Map) object, value); else ((Map) object).put(key, value); } else { // Set field on object. try { if (!config.allowDuplicates && keys.contains(key)) { throw new YamlReaderException("Duplicate key found '" + key + "'"); } keys.add(key); Property property = Beans.getProperty(type, (String) key, config.beanProperties, config.privateFields, config); if (property == null) { if (config.readConfig.ignoreUnknownProperties) continue; throw new YamlReaderException("Unable to find property '" + key + "' on class: " + type.getName()); } Class propertyElementType = config.propertyToElementType.get(property); if (propertyElementType == null) propertyElementType = property.getElementType(); Class propertyDefaultType = config.propertyToDefaultType.get(property); if (!isExplicitKey) value = readValue(property.getType(), propertyElementType, propertyDefaultType); property.set(object, value); } catch (Exception ex) { if (ex instanceof YamlReaderException) throw (YamlReaderException) ex; throw new YamlReaderException("Error setting property '" + key + "' on class: " + type.getName(), ex); } } } if (object instanceof DeferredConstruction) { try { object = ((DeferredConstruction) object).construct(); if (anchor != null) anchors.put(anchor, object); // Update anchor with real object. } catch (InvocationTargetException ex) { throw new YamlReaderException("Error creating object.", ex); } } return object; } case SEQUENCE_START: { // Must be a collection or an array. event = parser.getNextEvent(); Collection collection; if (Collection.class.isAssignableFrom(type)) { try { collection = (Collection) Beans.createObject(type, config.privateConstructors); } catch (InvocationTargetException ex) { throw new YamlReaderException("Error creating object.", ex); } } else if (type.isArray()) { collection = new ArrayList(); elementType = type.getComponentType(); } else throw new YamlReaderException("A sequence is not a valid value for the type: " + type.getName()); if (!type.isArray() && anchor != null) anchors.put(anchor, collection); while (true) { event = parser.peekNextEvent(); if (event.type == SEQUENCE_END) { parser.getNextEvent(); break; } collection.add(readValue(elementType, null, null)); } if (!type.isArray()) return collection; Object array = Array.newInstance(elementType, collection.size()); int i = 0; for (Object object : collection) Array.set(array, i++, object); if (anchor != null) anchors.put(anchor, array); return array; } case SCALAR: // Interpret an empty scalar as null. if (((ScalarEvent) event).value.length() == 0) { event = parser.getNextEvent(); return null; } // Fall through. default: throw new YamlReaderException("Expected data for a " + type.getName() + " field but found: " + event.type); } } /** * see http://yaml.org/type/merge.html */ @SuppressWarnings("unchecked") private void mergeMap(Map dest, Object source) throws YamlReaderException { if (source instanceof Collection) { for (Object item : ((Collection) source)) mergeMap(dest, item); } else if (source instanceof Map) { Map map = (Map) source; for (Map.Entry entry : map.entrySet()) { if (!dest.containsKey(entry.getKey())) dest.put(entry.getKey(), entry.getValue()); } } else throw new YamlReaderException("Expected a mapping or a sequence of mappings for a '<<' merge field but found: " + source.getClass().getSimpleName()); } /** * Returns a new object of the requested type. */ protected Object createObject(Class type) throws InvocationTargetException { // Use deferred construction if a non-zero-arg constructor is available. DeferredConstruction deferredConstruction = Beans.getDeferredConstruction(type, config); if (deferredConstruction != null) return deferredConstruction; return Beans.createObject(type, config.privateConstructors); } public class YamlReaderException extends YamlException { public YamlReaderException(String message, Throwable cause) { super("Line " + parser.getLineNumber() + ", column " + parser.getColumn() + ": " + message, cause); } public YamlReaderException(String message) { this(message, null); } } public static void main(String[] args) throws Exception { YamlReader reader = new YamlReader(new FileReader("test/test.yml")); System.out.println(reader.read()); } }