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

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

The newest version!
/**
    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.util.ArrayList;
import java.util.Collection;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import com.quinsoft.zeidon.ActivateOptions;
import com.quinsoft.zeidon.AttributeInstance;
import com.quinsoft.zeidon.CommitFlags;
import com.quinsoft.zeidon.CommitOptions;
import com.quinsoft.zeidon.Committer;
import com.quinsoft.zeidon.ObjectEngineEventListener;
import com.quinsoft.zeidon.OiSourceSelector;
import com.quinsoft.zeidon.SubobjectValidationException;
import com.quinsoft.zeidon.View;
import com.quinsoft.zeidon.ZeidonException;
import com.quinsoft.zeidon.objectdefinition.AttributeDef;
import com.quinsoft.zeidon.objectdefinition.DataRecord;
import com.quinsoft.zeidon.objectdefinition.EntityDef;
import com.quinsoft.zeidon.objectdefinition.LodDef;
import com.quinsoft.zeidon.objectdefinition.RelRecord;
import com.quinsoft.zeidon.utils.Timer;

/**
 * @author DG
 *
 */
class CommitMultipleOIs
{
    @SuppressWarnings("unused")
    private final EnumSet control;
    private       List       viewList;
    private final Collection     originalViewList;
    private final TaskImpl             task;
    private final CommitOptions        options;

    /**
     * This will hold the pairings of all the includes that
     * have explicit authority to do the include for all the OIs.
     */
    private Set includableRelationships;

    /**
     * This will hold the pairings of all the excludes that
     * have explicit authority to do the exclude for all the OIs.
     */
    private Set excludableRelationships;

    /**
     * This is used to determine where an OI is activated from.
     * Some day this may be provided by the OE options.
     */
    private final OiSourceSelector selector = new DefaultOiSourceSelector();

    private interface HasPermission
    {
        boolean hasPermission( EntityInstanceImpl ei );
    }

    private static final HasPermission hasDeletePermission = new HasPermission(){

        @Override
        public boolean hasPermission( EntityInstanceImpl ei )
        {
            return ei.getEntityDef().isDelete();
        }};

    private static final HasPermission hasUpdatePermission = new HasPermission(){

        @Override
        public boolean hasPermission( EntityInstanceImpl ei )
        {
            return ei.getEntityDef().isUpdate();
        }};

    private static final HasPermission hasCreatePermission = new HasPermission(){

        @Override
        public boolean hasPermission( EntityInstanceImpl ei )
        {
            return ei.getEntityDef().isCreate();
        }};

    private static final HasPermission hasIncludePermission = new HasPermission(){

        @Override
        public boolean hasPermission( EntityInstanceImpl ei )
        {
            return ei.getEntityDef().isInclude();
        }};

    private static final HasPermission hasExcludePermission = new HasPermission(){

        @Override
        public boolean hasPermission( EntityInstanceImpl ei )
        {
            return ei.getEntityDef().isExclude();
        }};

    CommitMultipleOIs(TaskImpl task, CommitOptions options, Collection views)
    {
        this.task = task;
        this.options = options;
        originalViewList = views;
        this.control = options.getControl();
    }

    private TaskImpl getTask()
    {
        return task;
    }

   /**
    *
    */
    private void setAutoSeq( final ObjectInstance oi )
    {
        // Set any autoseq attributes and find the last EI in the OI.
        for ( final EntityInstanceImpl ei : oi.getEntities( true ) )
        {
            final EntityDef entityDef = ei.getEntityDef();
            if ( entityDef.isDerivedPath() )
                continue;
            if ( ! entityDef.isUpdate() )
                continue;

            final AttributeDef autoSeq = entityDef.getAutoSeq();
            if ( autoSeq == null )
                continue;

            if ( ei.getPrevTwin() == null && // Must be first twin
                 ei.getNextTwin() != null )  // Don't bother if only one twin.
            {
                DataRecord dataRecord = entityDef.getDataRecord();
                RelRecord relRecord = dataRecord.getRelRecord();

                // Setting the autoseq for the child of a m-to-m relationship results in an update to
                // the m-to-m table, not the entity table.  Since we aren't really updating the record
                // we won't set the incremental flag.
                boolean setIncremental = (relRecord == null || relRecord.getRelationshipType() != RelRecord.MANY_TO_MANY);

                int seq = 1;
                for ( EntityInstanceImpl twin = ei; twin != null; twin = twin.getNextTwin() )
                {
                    if ( twin.isHidden() )
                        continue;

                    if ( twin.getAttribute( autoSeq ).setInternalValue( seq++, setIncremental ) )
                        twin.dbhSeqUpdated = true;

                    // Turn off the bDBHUpdated flag (if it's on) so that we
                    // make sure the entity is updated. If the entity instance
                    // is linked with someone else it's possible that the
                    // entity was updated through the other link.
                    twin.dbhUpdated = false;
                }
            }
        }
    }

    private void validateCommit()
    {
        // Build a list of the non-empty views.
        viewList = new ArrayList();
        for ( View v : originalViewList )
        {
            if ( v.isReadOnly() )
                throw new ZeidonException("Attempting to commit read-only view.  View = %s", v.getLodDef().getName() );

            LodDef lodDef = v.getLodDef();
            if ( ! lodDef.hasPhysicalMappings() )
                throw new ZeidonException("Attempting to commit OI with no physical mappings.  LOD = %s", lodDef.getName() );

            ViewImpl view = ((InternalView) v).getViewImpl();
            ObjectInstance oi = view.getObjectInstance();
            if ( oi.isVersioned() )
                throw new ZeidonException("Attempting to commit a view with outstanding versioned instances.  " +
                                          "View = %s", v );

            setAutoSeq( view.getObjectInstance() );
            viewList.add( view );
        }

        // Run the commit constraints before we do any validation because the constraints might
        // change the OIs.
        executeCommitConstraints();

        // Remove any empty OIs.
        Set oiSet = new HashSet();
        for ( Iterator iter = viewList.iterator(); iter.hasNext(); )
        {
            ViewImpl view = iter.next();
            ObjectInstance oi = view.getObjectInstance();

            // If this OI has no instances, remove it.
            if ( oi.getRootEntityInstance() == null )
            {
                iter.remove();
                continue;
            }

            oiSet.add( oi );
        }

        // Call validateOi on all the views.
        List validationExceptions = new ArrayList();
        for ( ViewImpl view : viewList )
        {
            Collection list = view.validateOi();
            if ( list != null )
                validationExceptions.addAll( list );
        }

        if ( validationExceptions.size() > 0 )
            throw new SubobjectValidationException( validationExceptions );

        validatePermissions( oiSet );
    }

    /**
     * See if any linked EI in one of the other OIs has the correct permission.
     *
     * @param ei
     * @param oiSet
     * @param permissionChecker
     * @return
     */
    private EntityInstanceImpl validatePermissionForEi( EntityInstanceImpl ei,
                                                        Set oiSet,
                                                        HasPermission permissionChecker,
                                                        Set permissionMap )
    {
        // Sanity check to make sure dbhLoaded is always off by the time we commit.
        assert ei.dbhLoaded == false : ei.toString() + " still has dbhLoaded flag on";

        // Does the EI have permission?
        if ( permissionChecker.hasPermission( ei ) )
            return ei;

        EntityInstanceImpl parent = ei.getParent();
        EntityDef entityDef = ei.getEntityDef();
        if ( permissionMap != null && parent != null )
        {
            // permissionMap has a pairing of all includes/excludes that have explicit
            // permission to do the include/exclude.  If we get here then ei doesn't
            // have explicit permission to do the include/exclude.  We'll use the
            // permission map to see if the same relationship exists in linked EIs
            // that does have permission.

            // Check relationship in one direction.
            if ( permissionMap.contains( entityDef.getErRelToken() ) )
                return parent;

            // Permission map is defined for includes/excludes.  If we don't have a match
            // then we have a problem.
            return null;
        }

        // If we get here then the EI doesn't have permission to perform its operation.
        // We need to go through all the entities linked with ei to see if *they* have
        // permission.  We'll start by creating a list of valid linked EIs.
        Set linkedEis = new HashSet<>();
        for ( EntityInstanceImpl linked : ei.getLinkedInstances() )
        {
            EntityDef linkedEntityDef = linked.getEntityDef();
            assert linkedEntityDef.getErEntityToken() == ei.getEntityDef().getErEntityToken() : "Mismatching ER tokens";

            if ( linkedEntityDef.isDerivedPath() )
                continue;

            // We only want EIs that are part of the OIs that are being committed.
            if ( ! oiSet.contains( linked.getObjectInstance() ) )
                continue;

            linkedEis.add( linked );
        }

        if ( linkedEis.size() == 0 )
            return null;

        // First check the simple case.  Do any of the linked EIs have the same parent
        // relationship with permission?
        for ( EntityInstanceImpl linked : linkedEis )
        {
            if ( permissionChecker.hasPermission( linked ) ) // Does the linked EI have the required authority?
            {
                // We found a linked instance that has the correct authority.
                return linked;
            }
        }

        return null;
    }

    /**
     * This creates two maps, one for includes and one for excludes.  Each map will contain
     * all the includes/excludes that have explicit permission (via EntityDef flags) to do
     * the include/exclude.
     *
     * The keys and values of the maps are taken from ei.getUniqueLinkedObject().  This allows
     * us to do a quick verification for authority for all linked EIs.
     */
    private void accumulatePermissionMaps()
    {
        includableRelationships = new HashSet<>();
        excludableRelationships = new HashSet<>();

        for ( ViewImpl view : viewList )
        {
            ObjectInstance oi = view.getObjectInstance();
            for ( EntityInstanceImpl ei : oi.getEntities( true ) )
            {
                EntityInstanceImpl parent = ei.getParent();

                if ( parent == null )
                    continue;

                EntityDef entityDef = ei.getEntityDef();
                if ( entityDef.isDerivedPath() )
                    continue;

                if ( entityDef.getDataRecord() == null )
                    continue;

                if ( ei.isIncluded() && entityDef.isInclude() )
                    includableRelationships.add( entityDef.getErRelToken() );  // Rel is includable.

                if ( ei.isExcluded() && entityDef.isExclude() )
                    excludableRelationships.add( entityDef.getErRelToken() );  // Rel is excludable.
            } // each entityInstance
        } // each view

    }

    private boolean assertChildUpdatedSetForParents( EntityInstanceImpl ei )
    {
        for ( EntityInstanceImpl parent = ei.getParent();
              parent != null && ! parent.isChildUpdated();
              parent = parent.getParent() )
        {
            if ( parent.isChildUpdated() )
                return false;
        }

        return true;
    }

    /**
     * For each entity instance that has been changed in all the OIs, make sure that
     * one of the OI's has the permission to create/delete/update/etc.
     *
     * @param oiSet
     */
    private void validatePermissions( Set oiSet )
    {
        boolean missingPermission = false;
        accumulatePermissionMaps();

        for ( ViewImpl view : viewList )
        {
            ObjectInstance oi = view.getObjectInstance();

            // Get the activate options to see if we are sharing a transaction.
            // activateOptions will be null on a new OI.
            ActivateOptions activateOptions = oi.getActivateOptions();
            if ( activateOptions != null && activateOptions.isSingleTransaction() )
                options.setSingleTransaction( true );

            for ( EntityInstanceImpl ei = oi.getRootEntityInstance();
                  ei != null;
                  ei = ei.getNextHier() )
            {
                EntityDef entityDef = ei.getEntityDef();

                // Unless we learn otherwise, assume this does not need to be committed.
                ei.dbhNeedsCommit = false;
                ei.dbhUpdateForeignKeysOnly = false;

                if ( entityDef.isDerivedPath() )
                    continue;

                if ( entityDef.getDataRecord() == null )
                    continue;

                if ( ei.isIncluded() && ! ei.isExcluded() )
                {
                    ei.dbhNeedsCommit = true;
                    assert assertChildUpdatedSetForParents( ei );
                    if ( ! entityDef.isInclude() && validatePermissionForEi( ei, oiSet, hasIncludePermission, includableRelationships ) == null )
                    {
                        missingPermission = true;
                        getTask().log().error( "Entity instance in view: %s  entity: %s  does not have include authority:", view, entityDef.getName() );
                        ei.logEntity();
                    }
                }

                if ( ei.isExcluded() && ! ei.isIncluded() )
                {
                    ei.dbhNeedsCommit = true;
                    assert assertChildUpdatedSetForParents( ei );
                    if ( ! entityDef.isExclude() && validatePermissionForEi( ei, oiSet, hasExcludePermission, excludableRelationships ) == null )
                    {
                        missingPermission = true;
                        getTask().log().error( "Entity instance in view: %s  entity: %s  does not have exclude authority:", view, entityDef.getName() );
                        ei.logEntity();
                    }
                }

                if ( ei.isCreated() && ! ei.isDeleted() )
                {
                    ei.dbhNeedsCommit = true;
                    assert assertChildUpdatedSetForParents( ei );
                    if ( ! entityDef.isCreate() && validatePermissionForEi( ei, oiSet, hasCreatePermission, null ) == null )
                    {
                        missingPermission = true;
                        getTask().log().error( "Entity instance in view: %s  entity: %s  does not have create authority:", view, entityDef.getName() );
                        ei.logEntity();
                    }
                }

                if ( ei.isDeleted() && ! ei.isCreated() )
                {
                    ei.dbhNeedsCommit = true;
                    assert assertChildUpdatedSetForParents( ei );
                    if ( ! entityDef.isDelete() && validatePermissionForEi( ei, oiSet, hasDeletePermission, null ) == null )
                    {
                        missingPermission = true;
                        getTask().log().error( "Entity instance in view: %s  entity: %s  does not have delete authority:", view, entityDef.getName() );
                        ei.logEntity();
                    }
                }

                if ( ei.isUpdated() && ! ei.isDeleted() && ! ei.isCreated() )
                {
                    if ( entityDef.isUpdate() )
                    {
                        ei.dbhNeedsCommit = true;
                        assert assertChildUpdatedSetForParents( ei );

                        // Make sure we can update the attributes.
                        validateAttributePermission( oiSet, ei );
                    }
                    else
                    {
                        // We don't have update authority.  Does a linked instance have update authority?
                        EntityInstanceImpl updateableEi = validatePermissionForEi( ei, oiSet, hasUpdatePermission, null );
                        if (  updateableEi == null )
                        {
                            // No linked EI has update authority.  One last check: is the ei the root of a read-only
                            // subobject?  If so then we'll perform include/exclude but we'll ignore the update.
                            if ( entityDef.isReadOnlySubobjectRoot() )
                            {
                                // Indicate that we only want to update FKs because they are part of the include/exclude
                                // but we don't have authority to update other values.
                                ei.dbhUpdateForeignKeysOnly = true;
                            }
                            else
                            {
                                missingPermission = true;
                                getTask().log().error( "Entity instance in view: %s  entity: %s  does not have update authority:", view, entityDef.getName() );
                                ei.logEntity();
                            }
                        }
                        else
                            validateAttributePermission( oiSet, updateableEi );
                    }
                }

                // We have the necessary permission for this entity.  If the root of a read-only suboject then
                // we won't check the permissions of the children (nor will we attempt to commit them).
                if ( entityDef.isReadOnlySubobjectRoot() ) {
                    ei = ei.getLastChildHier();
                    getTask().log().debug(  "Skipping commit of children under %s because it is flagged as readOnlyRelationshipRoot", entityDef.getName() );
                }

            } // each entityInstance
        } // each view

        if ( missingPermission )
            throw new ZeidonException( "Commit has an entity instance that doesn't have permission.  See log for more." );
    }

    /**
     * Validate that this attribute has update authority if it's updated.
     * TODO: This throws an exception.  Maybe capture the exceptions and return them.
     * @param oiSet
     */
    private boolean validateAttributePermission( Set oiSet, EntityInstanceImpl ei )
    {
        ei.getAttributes();
        for ( AttributeInstance attr : ei.getAttributes() )
        {
            if ( attr.isUpdated() )
                ((AttributeInstanceImpl) attr).validateUpdateAttribute( oiSet );
        }

        return false;
    }

    private void executeCommitConstraints()
    {
        // Call commit constraints.
        for ( ViewImpl view : viewList )
            view.getLodDef().executeCommitConstraint( view );
    }

    int commit()
    {
        validateCommit();

        // If there aren't any valid views then we have nothing to commit.
        if ( viewList.size() == 0 )
        	return 0;

        Committer committer = selector.getCommitter( getTask(), viewList, options );
        committer.init( task, viewList, options );

        final Timer timer = new Timer();
        ObjectEngineEventListener listener = task.getObjectEngine().getOeEventListener();
        try
        {
            committer.commit();
            listener.objectInstanceCommitted( originalViewList, timer.getMilliTime(), null );
        }
        catch ( Exception e )
        {
            ZeidonException ze = ZeidonException.wrapException( e );
            listener.objectInstanceCommitted( originalViewList, timer.getMilliTime(), e );
            throw ze;
        }
        finally
        {
            if ( timer.getMilliTime() > 2000 )
                task.log().info( "==> Commit took %s seconds", timer );
            else
                task.log().info( "==> Commit took %d milliseconds", timer.getMilliTime() );
        }

        return 0;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy