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

org.openmdx.state2.aop1.DateState_1 Maven / Gradle / Ivy

There is a newer version: 2.18.10
Show newest version
/*
 * ====================================================================
 * Project:     openMDX, http://www.openmdx.org/
 * Description: Date State
 * Owner:       OMEX AG, Switzerland, http://www.omex.ch
 * ====================================================================
 *
 * This software is published under the BSD license as listed below.
 * 
 * Copyright (c) 2008-2010, OMEX AG, Switzerland
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or
 * without modification, are permitted provided that the following
 * conditions are met:
 * 
 * * Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 * 
 * * Redistributions in binary form must reproduce the above copyright
 *   notice, this list of conditions and the following disclaimer in
 *   the documentation and/or other materials provided with the
 *   distribution.
 * 
 * * Neither the name of the openMDX team nor the names of its
 *   contributors may be used to endorse or promote products derived
 *   from this software without specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
 * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS
 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
 * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 * 
 * ------------------
 * 
 * This product includes software developed by other organizations as
 * listed in the NOTICE file.
 */
package org.openmdx.state2.aop1;

import static org.openmdx.base.accessor.cci.SystemAttributes.CREATED_AT;
import static org.openmdx.base.accessor.cci.SystemAttributes.CREATED_BY;
import static org.openmdx.base.accessor.cci.SystemAttributes.REMOVED_AT;
import static org.openmdx.base.accessor.cci.SystemAttributes.REMOVED_BY;
import static org.openmdx.state2.spi.TechnicalAttributes.STATE_VALID_FROM;
import static org.openmdx.state2.spi.TechnicalAttributes.STATE_VALID_TO;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;

import javax.jdo.JDOUserException;
import javax.xml.datatype.XMLGregorianCalendar;

import org.openmdx.base.accessor.cci.DataObject_1_0;
import org.openmdx.base.accessor.view.Interceptor_1;
import org.openmdx.base.accessor.view.ObjectView_1_0;
import org.openmdx.base.exception.RuntimeServiceException;
import org.openmdx.base.exception.ServiceException;
import org.openmdx.base.mof.cci.ModelElement_1_0;
import org.openmdx.base.mof.cci.ModelHelper;
import org.openmdx.base.mof.cci.Model_1_0;
import org.openmdx.base.mof.cci.Multiplicity;
import org.openmdx.base.persistence.cci.PersistenceHelper;
import org.openmdx.kernel.exception.BasicException;
import org.openmdx.state2.cci.DateStateContext;
import org.openmdx.state2.cci.ViewKind;
import org.openmdx.state2.spi.Order;
import org.w3c.spi.DatatypeFactories;

/**
 * Date State Plug-In
 */
public class DateState_1
    extends BasicState_1 
{

    /**
     * Constructor 
     * 
     * @param self
     * @throws ServiceException
     */
    public DateState_1(
        ObjectView_1_0 self, 
        Interceptor_1 next
    ) throws ServiceException{
        super(self, next);
    }

    /**
     * 
     */
    private static final XMLGregorianCalendar NULL = DatatypeFactories.xmlDatatypeFactory().newXMLGregorianCalendar();

    private static final List IGNORABLE_ATTRIBUTES = Arrays.asList(
        STATE_VALID_FROM, STATE_VALID_TO,
        CREATED_AT, CREATED_BY,
        REMOVED_AT, REMOVED_BY
    );
        
    /**
     * Compare two XMLGregorianCalendar values where null is
     * considered to be smaller than every other value.
     */
    private static final Comparator VALID_FROM_COMPARATOR = new Comparator () {

        @Override
        public int compare(
            XMLGregorianCalendar o1,
            XMLGregorianCalendar o2
        ) {
            return Order.compareValidFrom(o1, o2);
        }
    };


    //------------------------------------------------------------------------
    // Extends AbstractState_1
    //------------------------------------------------------------------------

    /* (non-Javadoc)
     * @see org.openmdx.state2.aop2.core.AbstractState_1#propagateValidTime()
     */
    @Override
    protected void initialize(
        DataObject_1_0 dataObject
    ) throws ServiceException {
        DateStateContext context = (DateStateContext) self.getInteractionSpec();
        if(dataObject.objGetValue(STATE_VALID_FROM) == null) {
            dataObject.objSetValue(STATE_VALID_FROM, context.getValidFrom());
        }
        if(dataObject.objGetValue(STATE_VALID_TO) == null) {
            dataObject.objSetValue(STATE_VALID_TO, context.getValidTo());
        }
    }

    /* (non-Javadoc)
     * @see org.openmdx.state2.aop2.core.BasicState_1#isValidTimeFeature(java.lang.String)
     */
    @Override
    protected boolean isValidTimeFeature(String featureName) {
        return STATE_VALID_FROM.equals(featureName) || STATE_VALID_TO.equals(featureName);
    }

    /* (non-Javadoc)
     * @see org.openmdx.state2.aop2.core.BasicState_1#objGetValue(java.lang.String)
     */
    @Override
    public Object objGetValue(
        String feature
    ) throws ServiceException {
        if(isValidTimeFeature(feature)){
            DateStateContext context = (DateStateContext) self.getInteractionSpec();
            if(context.getViewKind() == ViewKind.TIME_RANGE_VIEW) {
                if(STATE_VALID_FROM.equals(feature)) return  context.getValidFrom();
                if(STATE_VALID_TO.equals(feature)) return  context.getValidTo();
            }
        }
        return super.objGetValue(feature);
    }

    /* (non-Javadoc)
     * @see org.openmdx.state2.plugin.AbstractState_1#enableUpdate(java.util.Collection, int)
     */
    @Override
    protected void enableUpdate(
        Map pending
    ) throws ServiceException {
        Collection states = getStates();
        DateStateContext context = getContext();
        XMLGregorianCalendar validFrom;
        XMLGregorianCalendar validTo;
        for(Map.Entry e : pending.entrySet()) {
            DataObject_1_0 source = e.getKey();
            BoundaryCrossing boundaryCrossing = e.getValue();
            //
            // Handle the period which is not yet involved
            //
            if(boundaryCrossing.startsEarlier) {
                DataObject_1_0 predecessor = PersistenceHelper.clone(source);
                predecessor.objSetValue(
                    STATE_VALID_TO, 
                    Order.predecessor(
                        validFrom = context.getValidFrom()
                    )
                );
                if(!predecessor.jdoIsNew()) {
                    addState(states, predecessor);
                }
            } else {
                validFrom = NULL;
            }
            //
            // Handle the period which is not longer involved
            //
            if(boundaryCrossing.endsLater) {
                DataObject_1_0 successor = PersistenceHelper.clone(source);
                successor.objSetValue(
                    STATE_VALID_FROM, 
                    Order.successor(
                        validTo = context.getValidTo()
                    )
                );
                if(!successor.jdoIsNew()) {
                    addState(states, successor);
                }
            } else {
                validTo = NULL;
                
            }
            //
            // Handle the period which is involved
            //
            DataObject_1_0 target = PersistenceHelper.clone(source);
            if(validFrom != NULL) {
                target.objSetValue(STATE_VALID_FROM, validFrom);
            }
            if(validTo != NULL) {
                target.objSetValue(STATE_VALID_TO, validTo);
            }
            if(!target.jdoIsNew()) {
                addState(states, target);
            }
            //
            // Replace states
            //
            if(source.jdoIsPersistent()) {
                invalidate(source);
            } else {
                states.remove(source);
            }
        }
    }

	protected boolean addState(
		Collection states,
		DataObject_1_0 state
	) {
		try {
			return states.add(state);
		} catch (RuntimeException exception) {
			final BasicException stack = BasicException.toExceptionStack(exception);
			if(stack.getCause(null).getExceptionCode() == BasicException.Code.DUPLICATE) {
				final String message = "The technical property stateVersion has probably an incosistent value: It must not be smaller than the largest state number";
				throw new JDOUserException(
					message,	
					BasicException.newStandAloneExceptionStack(
						stack, 
						BasicException.Code.DEFAULT_DOMAIN, 
						BasicException.Code.ASSERTION_FAILURE, 
						message
					)
				);
			} else {
				throw exception;
			}
		}
	}

    /* (non-Javadoc)
     * @see org.openmdx.state2.plugin.AbstractState_1#exceedsTimeRangeLimits(org.openmdx.base.accessor.generic.cci.Object_1_0)
     */
    @Override
    protected BoundaryCrossing getBoundaryCrossing(
        DataObject_1_0 candidate
    ) throws ServiceException {
        DateStateContext context = getContext();
        return BoundaryCrossing.valueOf(
            Order.compareValidFrom(
                (XMLGregorianCalendar) candidate.objGetValue(STATE_VALID_FROM),
                context.getValidFrom() 
            ) < 0,
            Order.compareValidTo(
                (XMLGregorianCalendar) candidate.objGetValue(STATE_VALID_TO),
                context.getValidTo() 
            ) > 0
        );
    }

    /* (non-Javadoc)
     * @see org.openmdx.state2.aop1.BasicState_1#interfers(org.openmdx.base.accessor.cci.DataObject_1_0)
     */
    @Override
    protected boolean interfersWith(
        DataObject_1_0 candidate
    ) throws ServiceException {
        DateStateContext context = getContext();
        return 
            Order.compareValidFromToValidTo(context.getValidFrom(), (XMLGregorianCalendar) candidate.objGetValue(STATE_VALID_TO)) <= 0 &&
            Order.compareValidFromToValidTo((XMLGregorianCalendar) candidate.objGetValue(STATE_VALID_FROM), context.getValidTo()) <= 0;
    }

    /* (non-Javadoc)
     * @see org.openmdx.state2.aop1.AbstractState_1#isInvolved()
     */
    @Override
    protected boolean isInvolved(
        DataObject_1_0 candidate, 
        DateStateContext context, 
        AccessMode accessMode
    ) throws ServiceException {
        if(super.isInvolved(candidate, context, accessMode)) {
            //
            // Valid Time Test
            // 
            switch(context.getViewKind()) {
                case TIME_POINT_VIEW:
                    XMLGregorianCalendar validAt = context.getValidAt();
                    return Order.compareValidFrom(
                        (XMLGregorianCalendar) candidate.objGetValue(STATE_VALID_FROM),
                        validAt
                    ) <= 0 && Order.compareValidTo(
                        validAt,
                        (XMLGregorianCalendar) candidate.objGetValue(STATE_VALID_TO)
                    ) <= 0;
                case TIME_RANGE_VIEW:
                	return accessMode == AccessMode.UNDERLYING_STATE ? (
                        Order.compareValidFrom(
                            context.getValidFrom(), 
                            (XMLGregorianCalendar) candidate.objGetValue(STATE_VALID_FROM)
                        ) >= 0 && Order.compareValidTo(
                            (XMLGregorianCalendar) candidate.objGetValue(STATE_VALID_TO),
                            context.getValidTo()
                        ) >= 0 
                    ) : (
                		Order.compareValidFromToValidTo(
	                        context.getValidFrom(), 
	                        (XMLGregorianCalendar) candidate.objGetValue(STATE_VALID_TO)
	                    ) <= 0 && Order.compareValidFromToValidTo(
	                        (XMLGregorianCalendar) candidate.objGetValue(STATE_VALID_FROM),
	                        context.getValidTo()
	                    ) <= 0 
                    );
        		default:
        			throw new RuntimeServiceException(
        				BasicException.Code.DEFAULT_DOMAIN,
        				BasicException.Code.ASSERTION_FAILURE,
        				"Unexpected view kind",
        				new BasicException.Parameter("context", context)
        			);
            }
        } else {
	        return false;
        }
    }

    /**
     * Reduce number of states
     */
    @Override
    protected void reduceStates(
    ) throws ServiceException {
        final Collection states = getStates();
        reduceStatesToBeRemoved(states); // before merging
        if(reduceActiveStates(states)) { // merge
            reduceStatesToBeRemoved(states); // after merging
        }
    }

    /**
     * Merge similar adjacent states
     */
    private boolean reduceActiveStates(
        final Collection states
    ) throws ServiceException {
        boolean reduced = false;
        final SortedMap active = getActiveStates(states);
        if(active.size() > 1) {
            final Iterator i = active.values().iterator();
            final List merged = new ArrayList();
            for(
                DataObject_1_0 predecessor = i.next(), successor = null;
                predecessor != null;
                predecessor = successor
            ){
                final boolean merge;
                if(i.hasNext()) {
                    successor = i.next();
                    merge = adjacent(predecessor, successor) && similar(predecessor, successor);
                    if(merge) {
                        if(merged.isEmpty()) {
                            merged.add(predecessor);
                        }
                        merged.add(successor);
                    }
                } else {
                    merge = false;
                    successor = null;
                }
                if(!merge && !merged.isEmpty()) {
                    final DataObject_1_0 extendable = getExtendableState(merged);
                    //
                    // Avoid increasing the total number of states by merging
                    //
                    if(extendable != null) {
                        reduced = true;
                        //
                        // Merging reduces the number of states
                        //
                        extendable.objSetValue(
                            STATE_VALID_FROM,
                            merged.get(0).objGetValue(STATE_VALID_FROM)
                        );
                        extendable.objSetValue(
                            STATE_VALID_TO,
                            merged.get(merged.size()-1).objGetValue(STATE_VALID_TO)
                        );
                        for(DataObject_1_0 state : merged) {
                            if(state != extendable) {
                                invalidate(state);
                            }
                        }
                    }
                    merged.clear();
                }
            }
        }
        return reduced;
    }

    /**
     * Re-activate states similar to active states
     */
    private void reduceStatesToBeRemoved(
        final Collection states
    ) throws ServiceException {
        final SortedMap toBeRemoved = getStatesToBeRemoved(states);
        if(!toBeRemoved.isEmpty()) {
            final SortedMap newStates = getNewStates(states);
            if(!newStates.isEmpty()) { // Accessing the unmodifiable empty map would result in a ClassCastExcept
                for(Map.Entry e : toBeRemoved.entrySet()) {
                    final DataObject_1_0 newState = newStates.get(e.getKey());
                    if(newState != null) {
                        final DataObject_1_0 oldState = e.getValue();
                        final XMLGregorianCalendar newStateValidTo = (XMLGregorianCalendar)newState.objGetValue(STATE_VALID_TO);
                        final XMLGregorianCalendar oldStateValidTo = (XMLGregorianCalendar)oldState.objGetValue(STATE_VALID_TO);
                        if(
                            Order.compareValidTo(newStateValidTo,oldStateValidTo) == 0 &&
                            similar(newState, oldState)
                        ){
                            invalidate(newState);
                            reactivate(oldState);
                        }
                    }
                }
            }
        }
    }
    
    /**
     * Retrieve the active states
     * 
     * @param states the object's states
     * 
     * @return the active states ordered by {@code STATE_VALID_FROM}
     * 
     * @throws ServiceException if {@code STATE_VALID_FROM} can't be retrieved
     */
    private SortedMap getActiveStates(
        final Collection states
    ) throws ServiceException {
        final SortedMap activeStates = new TreeMap(VALID_FROM_COMPARATOR);
        for(DataObject_1_0 state : states){
            if(isActive(state)) {
                activeStates.put((XMLGregorianCalendar)state.objGetValue(STATE_VALID_FROM), state);
            }
        }
        return activeStates;
    }

    /**
     * Retrieve new states
     * 
     * @param states the object's states
     * 
     * @return new states ordered by {@code STATE_VALID_FROM}
     * 
     * @throws ServiceException if {@code STATE_VALID_FROM} can't be retrieved
     */
    private SortedMap getNewStates(
        final Collection states
    ) throws ServiceException {
        SortedMap newStates = null;
        for(DataObject_1_0 state : states){
            if(isNew(state)) {
                if(newStates == null) {
                    newStates = new TreeMap(VALID_FROM_COMPARATOR);
                }
                newStates.put((XMLGregorianCalendar)state.objGetValue(STATE_VALID_FROM), state);
            }
        }
        return newStates == null ? Collections.emptySortedMap() : newStates;
    }
    
    /**
     * Retrieve the states to be removed at the end of this unit of work 
     * 
     * @param states the object's states
     * 
     * @return the states to be removed ordered by {@code STATE_VALID_FROM}
     * 
     * @throws ServiceException if {@code STATE_VALID_FROM} can't be retrieved
     */
    private SortedMap getStatesToBeRemoved(
        final Collection states
    ) throws ServiceException {
        SortedMap toBeRemoved = null;
        for(DataObject_1_0 state : states){
            if(isToBeRemoved(state)){
                if(toBeRemoved == null) {
                    toBeRemoved = new TreeMap(VALID_FROM_COMPARATOR);
                }
                toBeRemoved.put((XMLGregorianCalendar)state.objGetValue(STATE_VALID_FROM), state);
            }
        }
        return toBeRemoved == null ? Collections.emptySortedMap() : toBeRemoved;
    }
    
    private DataObject_1_0 getExtendableState(
        final List merged
    ) {
        for(DataObject_1_0 state : merged) {
            if(state.jdoIsNew()) {
                return state;
            }
        }
        return null;
    }

    /**
     * Tells which attributes shall be ignored when comparing two states for similarity
     * 
     * @return the set of attributes to  be ignored when comparing two states for similarity
     */
    protected Collection ignorableAttributes(
    ){
        return IGNORABLE_ATTRIBUTES;
    }

    /**
     * Tells whether tow states follow each other immediately
     * 
     * @param left
     * @param right
     * 
     * @return true if the two states are dajacent
     * 
     * @throws ServiceException
     */
    protected boolean adjacent(
        DataObject_1_0 left,
        DataObject_1_0 right
    ) throws ServiceException{
        XMLGregorianCalendar leftEnd = (XMLGregorianCalendar) left.objGetValue(STATE_VALID_TO);
        XMLGregorianCalendar rightStart = (XMLGregorianCalendar) right.objGetValue(STATE_VALID_FROM);
        return rightStart.equals(Order.successor(leftEnd));
    }
    
    /**
     * Tests whether the two objects are similar
     * 
     * @param left
     * @param right
     * 
     * @return true if all attributes apart from the ones to be ignored are equal
     */
    protected boolean similar(
        DataObject_1_0 left,
        DataObject_1_0 right
    ) throws ServiceException{
        String type = left.objGetClass();
        if(!type.equals(right.objGetClass())) {
            return false;
        }
        Model_1_0 model = getModel();
        ModelElement_1_0 classifier = model.getElement(type);
        Map postponed = null;
        Set leftFetched = left.objDefaultFetchGroup();
        Set rightFetched = right.objDefaultFetchGroup();
        Collection ignorable = ignorableAttributes();
        for(Map.Entry feature : classifier.objGetMap("allFeature").entrySet()){
            ModelElement_1_0 featureDef = feature.getValue();
            String featureName = feature.getKey();
            if(!ignorable.contains(featureName) && this.isAttribute(featureDef) && !ModelHelper.isDerived(featureDef)) {
            	Multiplicity multiplicity = ModelHelper.getMultiplicity(featureDef);
                if(leftFetched.contains(featureName) && rightFetched.contains(featureName)) {
                	//
                	// Compare cached values first
                	//
                    if(!equal(featureName,multiplicity,left,right)) {
                        return false;
                    }
                } else {
                	//
                	// Compare values to be lazily fetched later on
                	//
                    if(postponed == null) {
                        postponed = new HashMap();
                    }
                    postponed.put(featureName, multiplicity);
                }
            }
        }
        if(postponed != null) {
            for(Map.Entry entry : postponed.entrySet()) {
                if(!equal(entry.getKey(),entry.getValue(),left,right)) {
                    return false;
                }
            }
        }
        return true;
    }

    /**
     * Tells whether a feature is either an attribute or a reference stored as attribute.
     * 
     * @param featureDef
     * @return true if the feature is either an attribute or a reference stored as attribute
     * 
     * @throws ServiceException
     */
    private boolean isAttribute(
    	ModelElement_1_0 featureDef
    ) throws ServiceException {
    	Model_1_0 model = featureDef.getModel();
    	return model.isAttributeType(featureDef) || (
    		model.isReferenceType(featureDef) && model.referenceIsStoredAsAttribute(featureDef)
    	);
    }
    /**
     * 
     * @param attribute
     * @param multiplicity
     * @param left
     * @param right
     * 
     * @return >true if the values are equal
     * @throws ServiceException
     */
    private boolean equal(
        String attribute, 
        Multiplicity multiplicity, 
        DataObject_1_0 left,
        DataObject_1_0 right
    ) throws ServiceException{
    	switch(multiplicity) {
	    	case LIST:
	    		return left.objGetList(attribute).equals(right.objGetList(attribute));
	    	case SET:
	    		return left.objGetSet(attribute).equals(right.objGetSet(attribute));
	    	case SPARSEARRAY:
	    		return left.objGetSparseArray(attribute).equals(right.objGetSparseArray(attribute));
	    	case MAP:
	    	    return left.objGetMap(attribute).equals(right.objGetMap(attribute));
	    	case STREAM:
	    	    return false; // we should not read streams in this context
	    	default:
	    		return equal(left.objGetValue(attribute),right.objGetValue(attribute));
    	}
    }
    
    /**
     * Tests whether the two objects are either equal or both null
     * 
     * @param left
     * @param right
     * 
     * @return true if the two objects are either equal or both null
     */
    static protected boolean equal(
        Object left,
        Object right
    ){
        if(left == null) {
            return right == null;
        } else {
            // Some datatype implementations don't accept null as equals argument!
            return right != null && left.equals(right);
        }
    }

}