org.openbravo.dal.core.OBInterceptor.java Source code

Java tutorial

Introduction

Here is the source code for org.openbravo.dal.core.OBInterceptor.java

Source

/*
 *************************************************************************
 * The contents of this file are subject to the Openbravo  Public  License
 * Version  1.1  (the  "License"),  being   the  Mozilla   Public  License
 * Version 1.1  with a permitted attribution clause; you may not  use this
 * file except in compliance with the License. You  may  obtain  a copy of
 * the License at http://www.openbravo.com/legal/license.html 
 * Software distributed under the License  is  distributed  on  an "AS IS"
 * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
 * License for the specific  language  governing  rights  and  limitations
 * under the License. 
 * The Original Code is Openbravo ERP. 
 * The Initial Developer of the Original Code is Openbravo SLU 
 * All portions are Copyright (C) 2008-2011 Openbravo SLU 
 * All Rights Reserved. 
 * Contributor(s):  ______________________________________.
 ************************************************************************
 */

package org.openbravo.dal.core;

import static org.openbravo.model.ad.system.Client.PROPERTY_ORGANIZATION;
import static org.openbravo.model.common.enterprise.Organization.PROPERTY_CLIENT;
import static org.openbravo.model.common.enterprise.Organization.PROPERTY_UPDATED;
import static org.openbravo.model.common.enterprise.Organization.PROPERTY_UPDATEDBY;

import java.io.Serializable;
import java.sql.SQLException;
import java.util.Date;
import java.util.Iterator;

import org.apache.log4j.Logger;
import org.hibernate.EmptyInterceptor;
import org.hibernate.Interceptor;
import org.hibernate.Transaction;
import org.hibernate.proxy.HibernateProxy;
import org.hibernate.type.Type;
import org.openbravo.base.exception.OBException;
import org.openbravo.base.exception.OBSecurityException;
import org.openbravo.base.model.BaseOBObjectDef;
import org.openbravo.base.model.Entity;
import org.openbravo.base.model.Property;
import org.openbravo.base.structure.BaseOBObject;
import org.openbravo.base.structure.ClientEnabled;
import org.openbravo.base.structure.OrganizationEnabled;
import org.openbravo.base.structure.Traceable;
import org.openbravo.dal.security.SecurityChecker;
import org.openbravo.model.ad.access.User;
import org.openbravo.model.ad.system.Client;
import org.openbravo.model.common.enterprise.Organization;

/**
 * This interceptor is used by Hibernate as a kind of save, update and delete event listener. This
 * event listener catches save or update events to set the client and organization and the
 * updated/created fields. In addition security checks are performed.
 * 
 * @author mtaal
 */

public class OBInterceptor extends EmptyInterceptor {
    private static final Logger log = Logger.getLogger(OBInterceptor.class);

    private static final long serialVersionUID = 1L;

    private static ThreadLocal<Boolean> preventUpdateInfoChange = new ThreadLocal<Boolean>();

    private static ThreadLocal<Boolean> disableCheckReferencedOrganizations = new ThreadLocal<Boolean>();

    /**
     * If true is passed and we are in adminMode then the update info (updated/updatedBy) is not
     * updated when an object gets updated.
     * 
     * @param value
     */
    public static void setPreventUpdateInfoChange(boolean value) {
        preventUpdateInfoChange.set(value);
    }

    /**
     * If true is passed then the checkReferencedOrganizations check is not done
     * 
     * @param value
     */
    public static void setDisableCheckReferencedOrganizations(boolean value) {
        disableCheckReferencedOrganizations.set(value);
    }

    private Interceptor interceptorListener;

    /**
     * Determines if the object is transient (==new and not yet persisted in Hibernate).
     * 
     * @param entity
     *          the object for which it is determined if it is new
     * @return true if the object has a null id or has been explicitly set to being new (@see
     *         BaseOBObject#isNewOBObject()}, returns false otherwise.
     */
    @Override
    public Boolean isTransient(Object entity) {
        // special case, if the id is set but it was explicitly
        // set to new then return new
        if (entity instanceof BaseOBObject) {
            final BaseOBObject bob = (BaseOBObject) entity;
            if (bob.getId() != null && bob.isNewOBObject()) {
                return new Boolean(true);
            }
        }
        // let hibernate do the rest
        return null;
    }

    /**
     * Performs security checks, is the user present in the {@link OBContext} allowed to delete this
     * entity and is the entity deletable (@see {@link Entity#isDeletable()}.
     * 
     * @param entity
     *          the business object which is deleted
     * @param id
     *          the id of the entity
     * @param state
     *          the value of the properties
     * @param propertyNames
     *          the name of the properties of the entity
     * @param types
     *          the hibernate type definition of the properties
     * 
     * @see BaseOBObject
     * @see Entity
     * @see Property
     */
    @Override
    public void onDelete(Object entity, Serializable id, Object[] state, String[] propertyNames, Type[] types) {
        SecurityChecker.getInstance().checkDeleteAllowed(entity);
        if (getInterceptorListener() != null) {
            getInterceptorListener().onDelete(entity, id, state, propertyNames, types);
        }
    }

    /**
     * Is called when the entity object is dirty (a value of a property has changed) and the state of
     * the object is about to be flushed to the database using sql update statements. This method
     * updates the audit info fields (updated, updatedBy) and performs security checks.
     * 
     * @param entity
     *          the business object which is deleted
     * @param id
     *          the id of the entity
     * @param currentState
     *          the current value of the properties
     * @param previousState
     *          the previous value of the properties, i.e. the values when the entity was loaded from
     *          the database
     * @param propertyNames
     *          the name of the properties of the entity
     * @param types
     *          the hibernate type definition of the properties
     * @return true if the state of the object has changed, this is the case when the entity has audit
     *         info because the updated/updatedBy properties are updated here, false is returned in
     *         other cases
     */
    @Override
    public boolean onFlushDirty(Object entity, Serializable id, Object[] currentState, Object[] previousState,
            String[] propertyNames, Type[] types) {

        // this can happen when someone has set the id of an object but has not set the
        // new object to true
        if (previousState == null) {
            log.warn("The object " + entity
                    + " is detected as not new (is its id != null?) but it does not have a current state in the database. "
                    + "This can happen when the id is set but not setNewObject(true); has been called.");
            return false;
        }
        // disabled for now, checks are all done when a property is set
        // if (entity instanceof BaseOBObject) {
        // ((BaseOBObject) entity).validate();
        // }

        doEvent(entity, currentState, propertyNames);
        if (disableCheckReferencedOrganizations.get() == null || !disableCheckReferencedOrganizations.get()) {
            checkReferencedOrganizations(entity, currentState, previousState, propertyNames);
        }

        boolean updated = false;
        if (getInterceptorListener() != null) {
            updated = getInterceptorListener().onFlushDirty(entity, id, currentState, previousState, propertyNames,
                    types);
        }

        if (entity instanceof Traceable || entity instanceof ClientEnabled
                || entity instanceof OrganizationEnabled) {
            return true;
        }
        return updated;
    }

    /**
     * Is called when a new entity object is persisted in the database. This method sets the audit
     * info fields (created/createdBy/updated/updatedBy) and performs several security checks.
     * 
     * @param entity
     *          the business object which is deleted
     * @param id
     *          the id of the entity
     * @param currentState
     *          the current value of the properties
     * @param propertyNames
     *          the name of the properties of the entity
     * @param types
     *          the hibernate type definition of the properties
     * @return true if the state of the object has changed, this is the case when the entity has audit
     *         info because the updated/updatedBy properties are updated here, false is returned in
     *         other cases
     */
    @Override
    public boolean onSave(Object entity, Serializable id, Object[] currentState, String[] propertyNames,
            Type[] types) {
        // disabled for now, checks are all done when a property is set
        // if (entity instanceof BaseOBObject) {
        // ((BaseOBObject) entity).validate();
        // }

        doEvent(entity, currentState, propertyNames);

        // also check for new records
        // commented out for now, re-apply in MP9
        // see issue https://issues.openbravo.com/view.php?id=19273
        // if (disableCheckReferencedOrganizations.get() == null
        // || !disableCheckReferencedOrganizations.get()) {
        // checkReferencedOrganizations(entity, currentState, new Object[currentState.length],
        // propertyNames);
        // }

        boolean listenerResult = false;
        if (getInterceptorListener() != null) {
            listenerResult = getInterceptorListener().onSave(entity, id, currentState, propertyNames, types);
        }

        // audit info fields
        if (entity instanceof Traceable || entity instanceof ClientEnabled
                || entity instanceof OrganizationEnabled) {
            return true;
        }
        return listenerResult || false;
    }

    private void checkReferencedOrganizations(Object entity, Object[] currentState, Object[] previousState,
            String[] propertyNames) {
        if (!(entity instanceof OrganizationEnabled)) {
            return;
        }
        final Organization o1 = ((OrganizationEnabled) entity).getOrganization();
        final OBContext obContext = OBContext.getOBContext();
        final BaseOBObject bob = (BaseOBObject) entity;
        boolean isNew = bob.getId() == null || bob.isNewOBObject();

        // check if the organization of the current object has changed, if so
        // then check all references
        if (!isNew) {
            for (int i = 0; i < currentState.length; i++) {
                if (propertyNames[i].equals(PROPERTY_ORGANIZATION)) {
                    if (currentState[i] != previousState[i]) {
                        isNew = true;
                        break;
                    }
                }
            }
        }

        for (int i = 0; i < currentState.length; i++) {
            // TODO maybe use equals
            if ((isNew || currentState[i] != previousState[i]) && !(currentState[i] instanceof Organization)
                    && (currentState[i] instanceof BaseOBObject || currentState[i] instanceof HibernateProxy)
                    && currentState[i] instanceof OrganizationEnabled) {
                // get the organization from the current state
                final OrganizationEnabled oe = (OrganizationEnabled) currentState[i];
                final Organization o2 = oe.getOrganization();

                // don't do the check for references to attribute set instance
                // using a hard coded name to prevent compile time dependencies
                // attribute set instances could be saved with the wrong org
                // see:
                // https://issues.openbravo.com/view.php?id=19272
                // to not block companies with wrong data prevent the check
                // in this case
                // also see:
                // https://issues.openbravo.com/view.php?id=19273
                final BaseOBObjectDef obObject = (BaseOBObjectDef) currentState[i];
                if (obObject.getEntity().getName().equals("AttributeSetInstance")) {
                    continue;
                }

                if (!obContext.getOrganizationStructureProvider(o1.getClient().getId()).isInNaturalTree(o1, o2)) {
                    throw new OBSecurityException("Entity " + bob.getIdentifier() + " (" + bob.getEntityName()
                            + ") with organization " + o1.getIdentifier() + " references an entity "
                            + ((BaseOBObject) currentState[i]).getIdentifier() + " through its property "
                            + propertyNames[i] + " but this referenced entity" + " belongs to an organization "
                            + o2.getIdentifier() + " which is not part of the natural tree of "
                            + o1.getIdentifier());
                }
            }
        }
    }

    // general event handler does new and update
    protected void doEvent(Object object, Object[] currentState, String[] propertyNames) {
        try {
            // not traceable but still do the security check
            if (!(object instanceof Traceable)) {
                // do a check for writeaccess
                // TODO: the question is if this is the correct
                // location as because of hibernate cascade many things are
                // written.
                SecurityChecker.getInstance().checkWriteAccess(object);
                return;
            }

            final Traceable t = (Traceable) object;
            if (t.getCreatedBy() == null) { // new
                onNew(t, propertyNames, currentState);
            } else {
                onUpdate(t, propertyNames, currentState);
            }
        } catch (Exception e) {
            while (e instanceof SQLException && e != ((SQLException) e).getNextException()) {
                e = ((SQLException) e).getNextException();
            }
            throw new OBException(e);
        }

        // do a check for writeaccess
        // TODO: the question is if this is the correct
        // location as because of hibernate cascade many things are written.
        SecurityChecker.getInstance().checkWriteAccess(object);
    }

    // set created/createdby and the client and organization
    private void onNew(Traceable traceable, String[] propertyNames, Object[] currentState) {
        // note both the currentState and the Traceable t are modified
        // the object t is modified because right after this call a security
        // check is done (see onSave). This is before hibernate can copy
        // the changes from currentState to the object. This happens slighlty later.
        final OBContext obContext = OBContext.getOBContext();
        final User currentUser = getCurrentUser();
        log.debug("OBEvent for new object " + traceable.getClass().getName() + " user " + currentUser.getName());

        Client client = null;
        Organization org = null;
        if (traceable instanceof ClientEnabled || traceable instanceof OrganizationEnabled) {
            // reread the client and organization
            client = SessionHandler.getInstance().find(Client.class, obContext.getCurrentClient().getId());
            org = SessionHandler.getInstance().find(Organization.class, obContext.getCurrentOrganization().getId());
        }
        final Date currentDate = new Date();
        for (int i = 0; i < propertyNames.length; i++) {
            // TODO: check why this is here, seems strange
            if ("".equals(propertyNames[i])) {
                currentState[i] = currentDate;
            }

            if (PROPERTY_UPDATED.equals(propertyNames[i])) {
                traceable.setUpdated(currentDate);
                currentState[i] = currentDate;
            }
            if (PROPERTY_UPDATEDBY.equals(propertyNames[i])) {
                traceable.setUpdatedBy(currentUser);
                currentState[i] = currentUser;
            }
            if (Organization.PROPERTY_CREATIONDATE.equals(propertyNames[i])) {
                traceable.setCreationDate(currentDate);
                currentState[i] = currentDate;
            }
            if (Organization.PROPERTY_CREATEDBY.equals(propertyNames[i])) {
                traceable.setCreatedBy(currentUser);
                currentState[i] = currentUser;
            }
            if (PROPERTY_CLIENT.equals(propertyNames[i]) && currentState[i] == null) {
                ((ClientEnabled) traceable).setClient(client);
                currentState[i] = client;
            }
            if (PROPERTY_ORGANIZATION.equals(propertyNames[i]) && currentState[i] == null) {
                ((OrganizationEnabled) traceable).setOrganization(org);
                currentState[i] = org;
            }
        }
    }

    // Sets the updated/updatedby
    // TODO: can the client/organization change?
    protected void onUpdate(Traceable t, String[] propertyNames, Object[] currentState) {
        if (OBContext.getOBContext().isInAdministratorMode() && preventUpdateInfoChange.get() != null
                && preventUpdateInfoChange.get()) {
            return;
        }
        final User currentUser = getCurrentUser();
        log.debug("OBEvent for updated object " + t.getClass().getName() + " user " + currentUser.getName());
        for (int i = 0; i < propertyNames.length; i++) {
            if (PROPERTY_UPDATED.equals(propertyNames[i])) {
                currentState[i] = new Date();
            }
            if (PROPERTY_UPDATEDBY.equals(propertyNames[i])) {
                currentState[i] = currentUser;
            }
        }
    }

    // after flushing an object is not new anymore
    @SuppressWarnings({ "rawtypes" })
    public void postFlush(Iterator entities) {
        while (entities.hasNext()) {
            final BaseOBObject bob = (BaseOBObject) entities.next();
            bob.setNewOBObject(false);
        }
    }

    // reads the current user from the database, note multiple reads
    // will hit the session cache
    private User getCurrentUser() {
        return SessionHandler.getInstance().find(User.class, OBContext.getOBContext().getUser().getId());
    }

    public Interceptor getInterceptorListener() {
        return interceptorListener;
    }

    public void setInterceptorListener(Interceptor interceptorListener) {
        this.interceptorListener = interceptorListener;
    }

    @Override
    public void afterTransactionBegin(Transaction tx) {
        if (getInterceptorListener() != null) {
            getInterceptorListener().afterTransactionBegin(tx);
        }
        super.afterTransactionBegin(tx);
    }

    @Override
    public void afterTransactionCompletion(Transaction tx) {
        if (getInterceptorListener() != null) {
            getInterceptorListener().afterTransactionCompletion(tx);
        }
        super.afterTransactionCompletion(tx);
    }

    @Override
    public void beforeTransactionCompletion(Transaction tx) {
        if (getInterceptorListener() != null) {
            getInterceptorListener().beforeTransactionCompletion(tx);
        }
        super.beforeTransactionCompletion(tx);
    }

    @SuppressWarnings({ "rawtypes" })
    @Override
    public void preFlush(Iterator entities) {
        if (getInterceptorListener() != null) {
            getInterceptorListener().preFlush(entities);
        }
        super.preFlush(entities);
    }

}