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

com.quinsoft.zeidon.standardoe.ActivateOisFromJsonStream Maven / Gradle / Ivy

The newest version!
/**
    This file is part of the Zeidon Java Object Engine (Zeidon JOE).

    Zeidon JOE 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 3 of the License, or
    (at your option) any later version.

    Zeidon JOE 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 Zeidon JOE.  If not, see .

    Copyright 2009-2015 QuinSoft
 */
package com.quinsoft.zeidon.standardoe;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.lang3.StringUtils;

import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonLocation;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.quinsoft.zeidon.Application;
import com.quinsoft.zeidon.CreateEntityFlags;
import com.quinsoft.zeidon.CursorPosition;
import com.quinsoft.zeidon.DeserializeOi;
import com.quinsoft.zeidon.SerializationMapping;
import com.quinsoft.zeidon.StreamReader;
import com.quinsoft.zeidon.Task;
import com.quinsoft.zeidon.View;
import com.quinsoft.zeidon.ZeidonException;
import com.quinsoft.zeidon.domains.Domain;
import com.quinsoft.zeidon.objectdefinition.AttributeDef;
import com.quinsoft.zeidon.objectdefinition.DynamicAttributeDefConfiguration;
import com.quinsoft.zeidon.objectdefinition.EntityDef;
import com.quinsoft.zeidon.objectdefinition.LodDef;

/**
 * Reads all the OIs from a given stream in JSON format.
 *
 * @author dgc
 *
 */
class ActivateOisFromJsonStream implements StreamReader
{
    private static final EnumSet CREATE_FLAGS = EnumSet.of( CreateEntityFlags.fNO_SPAWNING,
                                                                               CreateEntityFlags.fIGNORE_MAX_CARDINALITY,
                                                                               CreateEntityFlags.fDONT_UPDATE_OI,
                                                                               CreateEntityFlags.fDONT_INITIALIZE_ATTRIBUTES,
                                                                               CreateEntityFlags.fIGNORE_PERMISSIONS );
    private Task                    task;
    private InputStream             stream;

    /**
     * Keep track of the options for this activate.
     */
    private DeserializeOi  options;

    /**
     * This keeps track of all the entities that are the sources for linked instances.
     * The key is the EntityKey.
     */
    private final Map linkSources;

    private JsonParser                    jp;
    private Application                   application;
    private boolean                       incremental;
    private LodDef                        lodDef;
    private View                          view;
    private final List              returnList;
    private String version;

    /**
     * Used to keep track of the instances that are flagged as selected in the input
     * stream.  Cursors will be set afterwards.
     */
    private List selectedInstances;

    private Map entityMetas = new HashMap<>();

    /**
     * If true then mark the OI that is being read as readonly.
     */
    private boolean readOnlyOi;

    /**
     * If trure then mark the view as readonly.
     */
    private boolean readOnly;
    private boolean locked;
    private Integer totalRootCount;
    private SerializationMapping mapper;

    /**
     * A JSON stream will have a version.  Once the version is read from the stream a
     * subclass of JsonReader will be used to read a particular version.
     * @author dgc
     *
     */
    private interface JsonReader
    {
        void process() throws Exception;
    }

    ActivateOisFromJsonStream( )
    {
        returnList = new ArrayList();
        linkSources = new HashMap();
    }

    public List read()
    {
        mapper = options.getSerializationMapping();
        try
        {
            JsonFactory jsonFactory = new JsonFactory();
            jp = jsonFactory.createParser( stream );
            jp.configure( JsonParser.Feature.AUTO_CLOSE_SOURCE, false );

            // Read the START_OBJECT
            JsonToken token = jp.nextToken();
            if ( token != JsonToken.START_OBJECT )
                throw new ZeidonException( "OI JSON stream doesn't start with object." );

            token = jp.nextToken();
            if ( token != JsonToken.FIELD_NAME )
                throw new ZeidonException( "OI JSON missing OI field name." );

            String fieldName = jp.getCurrentName();
            if ( fieldName.equals( ".meta"  ) )
            {
                readFileMeta();

                JsonReader reader = getReaderForVersion();
                reader.process();
            }
            else
            {
                if ( StringUtils.equalsIgnoreCase( fieldName, "version" ) )
                {
                    token = jp.nextToken(); // Move to value.
                    version = jp.getValueAsString();
                    token = jp.nextToken(); // Move to next field name.
                    assert token == JsonToken.FIELD_NAME;
                    fieldName = jp.getCurrentName();
                }
                else
                if ( StringUtils.isBlank( options.getVersion() ) )
                {
                    version = "1";
                }

                totalRootCount = null;
                if ( StringUtils.equalsIgnoreCase( fieldName, "totalRootCount" ) )
                {
                    token = jp.nextToken(); // Move to value.
                    totalRootCount = jp.getValueAsInt();
                    token = jp.nextToken(); // Move to next field name.
                    assert token == JsonToken.FIELD_NAME;
                    fieldName = jp.getCurrentName();
                }

                if ( lodDef == null )
                    throw new ZeidonException( "JSON stream appears to start with the root entity name (%s)" +
                                               " but the LodDef has not been specified.", fieldName );

                EntityDef entityDef = mapper.getEntityFromRecord( fieldName, null, lodDef );
                if ( entityDef == null )
                    throw new ZeidonException( "The first field in the JSON stream must be the root entity" +
                                               " or '.meta' but was %s.", fieldName );

                view = task.activateEmptyObjectInstance( lodDef );
                returnList.add( view );
                if ( totalRootCount != null )
                    view.setTotalRootCount( totalRootCount );

                JsonReader reader = getSimpleReaderForVersion();
                reader.process();
            }

            jp.close();
        }
        catch ( Exception e )
        {
            ZeidonException ze = ZeidonException.wrapException( e );
            JsonLocation loc = jp.getCurrentLocation();
            JsonToken token = jp.getCurrentToken();
            ze.appendMessage( "Position line=%d col=%d, token=%s", loc.getLineNr(), loc.getColumnNr(),
                              token == null ? "No Token" : token.name() );
            throw ze;
        }

        return returnList;
    }

    private JsonReader getReaderForVersion()
    {
        String v = getVersion();
        switch ( v )
        {
            case "1":
            case "1.0":
                return new JsonReaderVersion1();

            default:
                throw new ZeidonException("Unknown version %s", v );
        }
    }

    private JsonReader getSimpleReaderForVersion()
    {
        String v = getVersion();
        switch ( v )
        {
            case "1":
                return new SimpleJsonReaderVersion1();

            default:
                throw new ZeidonException("Unknown version %s", v );
        }
    }

    private void readFileMeta() throws Exception
    {
        jp.nextToken();
        while ( jp.nextToken() != JsonToken.END_OBJECT )
        {
            String fieldName = jp.getCurrentName();
            jp.nextToken(); // Move to value.
            switch ( fieldName )
            {
                case "version":
                    version = jp.getValueAsString();
                    task.log().debug( "JSON version: %s", version );
                    break;

                case "date":
                    break;

                default:
                    task.log().warn( "Unknown .oimeta fieldname %s", fieldName );
            }
        }

        jp.nextToken();

    }

    private String getVersion()
    {
        if ( ! StringUtils.isBlank( options.getVersion() ) )
            return options.getVersion();

        if ( StringUtils.isBlank( version ) )
            throw new ZeidonException( "Version is not specified in stream or Deserialization options" );

        return version;
    }

    private boolean readOi() throws Exception
    {
        JsonToken token = jp.nextToken();

        // If we find the end of the OI array then that's the end of OIs.
        if ( token == JsonToken.END_ARRAY )
            return false;  // No more OIs in the stream.

        if ( token != JsonToken.START_OBJECT )
            throw new ZeidonException( "OI JSON stream doesn't start with object." );

        token = jp.nextToken();

        String fieldName = jp.getCurrentName();
        if ( StringUtils.equals( fieldName, ".oimeta" ) )
            token = readOiMeta();
        else
            throw new ZeidonException( ".oimeta object not specified in JSON stream" );

        // If the token after reading the .oimeta is END_OBJECT then the OI is empty.
        if ( token != JsonToken.END_OBJECT )
        {
            fieldName = jp.getCurrentName();
            EntityDef entityDef = mapper.getEntityFromRecord( fieldName, null, lodDef );
            if ( entityDef == null )
                throw new ZeidonException( "First entity specified in OI (%s) is not the root (%s)", fieldName,
                                           lodDef.getRoot().getName() );

            readEntity( fieldName, null ); // Null means we're loading the root.
            token = jp.nextToken();
        }

        // Now that we've updated everything, set the flags.
        if ( incremental )
        {
            for ( EntityInstanceImpl ei : entityMetas.keySet() )
            {
                EntityMeta entityMeta = entityMetas.get( ei );
                ei.setCreated( entityMeta.created );
                ei.setUpdated( entityMeta.updated );
                ei.setDeleted( entityMeta.deleted );
                ei.setIncluded( entityMeta.included );
                ei.setExcluded( entityMeta.excluded );
                if ( entityMeta.incomplete )
                    ei.setIncomplete( null );
                if ( entityMeta.lazyLoaded != null )
                {
                    String[] names = entityMeta.lazyLoaded.split( "," );
                    for ( String name: names )
                        ei.getEntitiesLoadedLazily().add( lodDef.getEntityDef( name, true, true ) );
                }
            }
        }

        if ( selectedInstances.size() > 0 )
            setCursors();
        else
            view.reset();

        if ( token != JsonToken.END_OBJECT )
            throw new ZeidonException( "OI JSON stream doesn't end with object." );

        ObjectInstance oi = ((InternalView) view).getViewImpl().getObjectInstance();
        if ( readOnlyOi )
            oi.setReadOnly( true );

        if ( locked )
            oi.setLocked( true );

        if ( readOnly )
            view.setReadOnly( true );

        if ( totalRootCount != null )
            view.setTotalRootCount( totalRootCount );

        return true;  // Keep looking for OIs in the stream.
    }

    /**
     * The view has been loaded from the stream and it was indicated that there are
     * cursor selections.  Reset them.
     */
    private void setCursors()
    {
        for ( EntityInstanceImpl ei : selectedInstances )
        {
            EntityDef entityDef = ei.getEntityDef();
            EntityCursorImpl cursor = (EntityCursorImpl) view.cursor( entityDef );

            // Use setEntityInstance() because we are setting all cursors.  This is
            // faster than using setCursor().
            cursor.setEntityInstance( ei );
        }
    }

    private boolean readSimpleOi() throws Exception
    {
        JsonToken token = jp.getCurrentToken();

        // If we find the end of the OI array then that's the end of OIs.
        if ( token == JsonToken.END_ARRAY || token == JsonToken.END_OBJECT )
            return false;  // No more OIs in the stream.

        String fieldName = jp.getCurrentName();

        assert token == JsonToken.FIELD_NAME;

        // If the token after reading the .oimeta is END_OBJECT then the OI is empty.
        if ( token != JsonToken.END_OBJECT )
        {
            // readEntity expects the current token to be the opening { or [.
            // Skip over the field name.
            token = jp.nextToken();
            readEntity( fieldName, null );
            token = jp.nextToken();
        }

        if ( token != JsonToken.END_OBJECT )
            throw new ZeidonException( "OI JSON stream doesn't end with object." );

        return true;  // Keep looking for OIs in the stream.
    }

    private void readEntity( String entityName, EntityDef parent ) throws Exception
    {
        // Keeps track of whether the entity list starts with a [ or not.  If there
        // is no [ then we are done reading entities of this type when we find the
        // end of the object.
        boolean entityArray = false;
        int twinCount = 0;

        JsonToken token = jp.getCurrentToken();
        if ( token == JsonToken.START_ARRAY )
        {
            token = jp.nextToken();
            entityArray = true;  // Entity list started with [
        }

        assert token == JsonToken.START_OBJECT;

        EntityDef entityDef = mapper.getEntityFromRecord( entityName, parent, lodDef, true );
        EntityInstanceImpl ei = null;

        // Read tokens until we find the token that ends the current list of entities.
        while ( ( token = jp.nextToken() ) != null )
        {
            if ( token == JsonToken.END_ARRAY )
                break;

            if ( ei == null )
            {
                ei = (EntityInstanceImpl) view.cursor( entityDef ).createEntity( CursorPosition.LAST, CREATE_FLAGS );
                twinCount++;
            }

            if ( token == JsonToken.END_OBJECT )
            {
                // If we get here then this could indicate an empty EI.  Get the next
                // token and get out if it's END_ARRAY.  Otherwise it better be the
                // start of another object.
                token = jp.nextToken();
                if ( token == JsonToken.END_ARRAY )
                    break;

                assert token == JsonToken.START_OBJECT;
                assert entityArray;  // If next token is an object then we better be reading an array.

                // Indicate that we want a new EI created.
                ei = null;
                continue;
            }

            // If there are multiple twins then the token is START_OBJECT to
            // indicate a new EI.
            if ( token == JsonToken.START_OBJECT )
            {
                assert twinCount > 1; // Assert that we already created at least one EI.
                continue;
            }

            assert token == JsonToken.FIELD_NAME;

            List attributeMetas = new ArrayList<>();

            // Read tokens until we find the token that ends the current entity.
            EntityMeta entityMeta = null;
            while ( ( token = jp.nextToken() ) != JsonToken.END_OBJECT )
            {
                String fieldName = jp.getCurrentName();

                if ( token == JsonToken.FIELD_NAME || token == JsonToken.START_OBJECT )
                    token = jp.nextToken();

                if ( StringUtils.equals( fieldName, ".meta" ) )
                {
                    entityMeta = readEntityMeta( ei );

                    // Now that we have everything we can perform some processing.
                    if ( entityMeta.isLinkedSource )
                        linkSources.put( entityMeta.entityKey, ei );
                    else
                    if ( entityMeta.linkedSource != null )
                        ei.linkInstances( linkSources.get( entityMeta.linkedSource ) );

                    continue;
                }

                if ( fieldName.startsWith( "." ) )
                {
                    AttributeMeta am = readAttributeMeta( ei, fieldName );
                    attributeMetas.add( am );
                    continue;
                }

                // Is this the start of an entity.
                if ( token == JsonToken.START_ARRAY || token == JsonToken.START_OBJECT )
                {
                    boolean recursiveChild = false;

                    // Validate that the entity name is valid.
                    EntityDef childEntity = mapper.getEntityFromRecord( fieldName, entityDef, lodDef, true );
                    if ( childEntity.getParent() != entityDef )
                    {
                        // Check to see the childEntity is a recursive child.
                        if ( entityDef.isRecursive() )
                        {
                            view.cursor( entityDef ).setToSubobject();
                            recursiveChild = true;
                        }
                        else
                            throw new ZeidonException( "Parse error: %s is not a child of %s", fieldName,
                                                       entityName );
                    }

                    readEntity( fieldName, entityDef );

                    if ( recursiveChild )
                        view.resetSubobject();

                    continue;
                }

                if ( StringUtils.equals( jp.getText(), fieldName ) )
                    // If jp points to attr name, get next token.
                    token = jp.nextToken();

                // This better be an attribute
                // Try getting the attribute.  We won't throw an exception (yet) if there
                // is no attribute with a matching name.
                AttributeDef attributeDef = mapper.getAttributeFromField( fieldName, entityDef );

                if ( attributeDef == null )
                {
                    // We didn't find an attribute with a name matching fieldName.  Do we allow
                    // dynamic attributes for this entity?
                    if ( options.getAllowableDynamicEntities() == null ||
                       ! options.getAllowableDynamicEntities().contains( entityDef.getName() ) )
                    {
                        entityDef.getAttribute( fieldName ); // This will throw the exception.
                    }

                    // We are allowing dynamic attributes.  Create one.
                    DynamicAttributeDefConfiguration config = new DynamicAttributeDefConfiguration();
                    config.setAttributeName( fieldName );
                    attributeDef = entityDef.createDynamicAttributeDef( config );
                }
                else
                if ( attributeDef.isDerived() ) // We'll ignore derived attributes.
                    continue;

                Domain domain = attributeDef.getDomain();
                Object internalValue = domain.convertExternalValue( task, ei.getAttribute( attributeDef ),
                                                                    attributeDef, null, jp.getText() );
                ei.getAttribute( attributeDef ).setInternalValue( internalValue, false );
                if ( incremental )
                {
                    // Since incremental flags are set, assume the attribute hasn't been
                    // updated.  We'll be told later if it has.
                    AttributeValue attrib = ei.getInternalAttribute( attributeDef );
                    attrib.setUpdated( false );
                }
                else
                {
                    // If we just set the key then we'll assume the entity has
                    // already been created.
                    if ( attributeDef.isKey() )
                        ei.setIncrementalFlags( IncrementalEntityFlags.UPDATED );
                }
            } // while ( ( token = jp.nextToken() ) != JsonToken.END_OBJECT )...

            // Apply all the attribute metas to correctly set the attribute flags.
            for ( AttributeMeta am : attributeMetas )
                am.apply( ei );

            if ( entityMeta == null )
            {
                ei.setCreated( false );
                ei.setUpdated( false );
            }

            // If the entity list didn't start with a [ then there is only one entity
            // in the list of twins so exit.
            if ( entityArray == false )
                break;

            // Indicate that we're done with this entity and we'll want a new one if
            // we find an OBJECT_START token.
            ei = null;
        } // while ( ( token = jp.nextToken() ) != null )...
    }

    private AttributeMeta readAttributeMeta( EntityInstanceImpl ei, String fieldName ) throws JsonParseException, IOException
    {
        String attribName = fieldName.substring( 1 ); // Remove the ".".
        AttributeMeta meta = new AttributeMeta();
        meta.attributeDef = ei.getEntityDef().getAttribute( attribName, true, true );

        while ( jp.nextToken() != JsonToken.END_OBJECT )
        {
            fieldName = jp.getCurrentName();

            if ( fieldName.equals( "updated" ) )
                meta.updated = true;
            else
                task.log().warn( "Unknown entity meta value %s", fieldName );
        }

        return meta;
    }

    private class AttributeMeta
    {
        private AttributeDef attributeDef;
        private boolean updated = false;

        private void apply( EntityInstanceImpl ei )
        {
            if ( updated )
            {
                AttributeValue attrib = ei.getInternalAttribute( attributeDef );
                attrib.setUpdated( true );
            }
        }
    }

    private EntityMeta readEntityMeta(EntityInstanceImpl ei) throws Exception
    {
        EntityMeta meta = new EntityMeta();
        while ( jp.nextToken() != JsonToken.END_OBJECT )
        {
            String fieldName = jp.getCurrentName();

            switch ( fieldName )
            {
                case  "incrementals" :   readIncrementals( meta ); break;
                case  "isLinkedSource" : meta.isLinkedSource = true; break;
                case  "entityKey" :      meta.entityKey = jp.getText(); break;
                case  "linkedSource" :   meta.linkedSource = jp.getText(); break;
                case  "selected" :       selectedInstances.add( ei ); break;
                case  "incomplete" :     meta.incomplete = true; break;
                case  "lazyLoaded" :     meta.lazyLoaded = jp.getText(); break;

                default: task.log().warn( "Unknown entity meta value %s", fieldName );
            }
        }

        entityMetas.put( ei, meta );
        return meta;
    }

    private void readIncrementals( EntityMeta meta ) throws JsonParseException, IOException
    {
        String increStr = jp.getText().toLowerCase();

        meta.updated  = increStr.contains( "u" );
        meta.created  = increStr.contains( "c" );
        meta.deleted  = increStr.contains( "d" );
        meta.included = increStr.contains( "i" );
        meta.excluded = increStr.contains( "x" );
    }

    private JsonToken readOiMeta() throws Exception
    {
        String odName = null;
        readOnlyOi = false;
        readOnly   = false;
        locked     = false;
        totalRootCount = null;

        jp.nextToken();
        while ( jp.nextToken() != JsonToken.END_OBJECT )
        {
            String fieldName = jp.getCurrentName();
            jp.nextToken(); // Move to value.
            switch ( fieldName )
            {
                case "application": application = task.getApplication( jp.getValueAsString() ); break;
                case "odName":      odName = jp.getValueAsString(); break; // Save OD name for later.
                case "incremental": incremental = jp.getValueAsBoolean(); break;
                case "readOnlyOi":  readOnlyOi = jp.getValueAsBoolean(); break;
                case "readOnly":    readOnly = jp.getValueAsBoolean(); break;
                case "locked":      locked = jp.getValueAsBoolean(); break;
                case "totalRootCount": totalRootCount = jp.getValueAsInt(); break;

                default: task.log().warn( "Unknown .oimeta fieldname %s", fieldName ); break;
            }
        }

        if ( odName == null )
            throw new ZeidonException( "LodDef not specified in JSON .oimeta" );

        // We don't load the LodDef until now because it's valid JSON to reorder
        // the values in the .oimeta object.
        lodDef = application.getLodDef( task, odName );
        view = task.activateEmptyObjectInstance( lodDef );
        returnList.add( view );
        JsonToken token = jp.nextToken();

        // Create a list to keep track of selected instances.
        selectedInstances = new ArrayList<>();

        // If the next token is FIELD_NAME then OI data is next so get the next token.
        // If it's not the the OI is EMPTY and token should be END_OBJECT.
        if ( token == JsonToken.FIELD_NAME )
            token = jp.nextToken();
        else
            assert token == JsonToken.END_OBJECT;

        return token;
    }

    private static class EntityMeta
    {
        private String lazyLoaded = null;
        private String linkedSource;
        private String entityKey;
        private boolean isLinkedSource;
        private boolean updated  = false;
        private boolean created  = false;
        private boolean deleted  = false;
        private boolean included = false;
        private boolean excluded = false;
        private boolean incomplete = false;
    }

    @Override
    public List readFromStream( DeserializeOi options )
    {
        this.task = options.getTask();
        this.stream = options.getInputStream();
        this.options = options;
        lodDef = options.getLodDef();
        return read();
    }

    private class JsonReaderVersion1 implements JsonReader
    {
        @Override
        public void process() throws Exception
        {
            JsonToken token = jp.nextToken();
            if ( token != JsonToken.START_ARRAY )
                throw new ZeidonException( "OI JSON missing beginning of OI array." );

            while ( readOi() );
        }
    }


    private class SimpleJsonReaderVersion1 implements JsonReader
    {
        @Override
        public void process() throws Exception
        {
            while ( readSimpleOi() );
        }
    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy