com.haulmont.cuba.security.app.EntityLog.java Source code

Java tutorial

Introduction

Here is the source code for com.haulmont.cuba.security.app.EntityLog.java

Source

/*
 * Copyright (c) 2008-2016 Haulmont.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */
package com.haulmont.cuba.security.app;

import com.haulmont.bali.util.Preconditions;
import com.haulmont.chile.core.datatypes.Datatypes;
import com.haulmont.chile.core.model.Instance;
import com.haulmont.chile.core.model.MetaClass;
import com.haulmont.chile.core.model.MetaProperty;
import com.haulmont.chile.core.model.Range;
import com.haulmont.cuba.core.*;
import com.haulmont.cuba.core.app.ServerConfig;
import com.haulmont.cuba.core.app.dynamicattributes.DynamicAttributes;
import com.haulmont.cuba.core.app.dynamicattributes.DynamicAttributesUtils;
import com.haulmont.cuba.core.entity.*;
import com.haulmont.cuba.core.global.*;
import com.haulmont.cuba.core.sys.AppContext;
import com.haulmont.cuba.core.sys.EntityManagerContext;
import com.haulmont.cuba.core.sys.persistence.EntityAttributeChanges;
import com.haulmont.cuba.security.entity.*;
import org.apache.commons.lang.BooleanUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import javax.annotation.Nullable;
import javax.annotation.concurrent.GuardedBy;
import javax.inject.Inject;
import java.io.IOException;
import java.io.StringWriter;
import java.util.*;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.stream.Collectors;

@Component(EntityLogAPI.NAME)
public class EntityLog implements EntityLogAPI {

    private static final Logger log = LoggerFactory.getLogger(EntityLog.class);

    @Inject
    protected TimeSource timeSource;
    @Inject
    protected Persistence persistence;
    @Inject
    protected Metadata metadata;
    @Inject
    protected MetadataTools metadataTools;
    @Inject
    protected UserSessionSource userSessionSource;
    @Inject
    protected ReferenceToEntitySupport referenceToEntitySupport;
    @Inject
    protected DynamicAttributes dynamicAttributes;
    @Inject
    protected DataManager dataManager;
    @Inject
    protected ServerConfig serverConfig;

    protected volatile boolean loaded;
    protected EntityLogConfig config;

    @GuardedBy("lock")
    protected Map<String, Set<String>> entitiesManual;
    @GuardedBy("lock")
    protected Map<String, Set<String>> entitiesAuto;

    protected ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    protected ThreadLocal<Boolean> entityLogSwitchedOn = new ThreadLocal<>();

    @Inject
    public EntityLog(Configuration configuration) {
        config = configuration.getConfig(EntityLogConfig.class);
    }

    @Override
    public void processLoggingForCurrentThread(boolean enabled) {
        entityLogSwitchedOn.set(enabled);
    }

    @Override
    public boolean isLoggingForCurrentThread() {
        return !Boolean.FALSE.equals(entityLogSwitchedOn.get());
    }

    @Override
    public void flush() {
        EntityManagerContext context = persistence.getEntityManagerContext();
        List<EntityLogItem> items = context.getAttribute(EntityLog.class.getName());
        if (items == null || items.isEmpty())
            return;

        for (EntityLogItem item : items) {
            List<EntityLogItem> sameEntityList = items.stream()
                    .filter(entityLogItem -> entityLogItem.getObjectEntityId().equals(item.getObjectEntityId()))
                    .collect(Collectors.toList());
            EntityLogItem itemToSave = sameEntityList.get(0);
            computeChanges(itemToSave, sameEntityList);
            saveItem(itemToSave);
        }
    }

    protected void computeChanges(EntityLogItem itemToSave, List<EntityLogItem> sameEntityList) {
        Set<String> allAttributes = sameEntityList.stream()
                .flatMap(entityLogItem -> entityLogItem.getAttributes().stream().map(EntityLogAttr::getName))
                .collect(Collectors.toSet());

        for (String attributeName : allAttributes) {
            // old value from the first item
            sameEntityList.get(0).getAttributes().stream()
                    .filter(entityLogAttr -> entityLogAttr.getName().equals(attributeName)).findFirst()
                    .ifPresent(entityLogAttr -> setAttributeOldValue(entityLogAttr, itemToSave));
            // new value from the last item
            sameEntityList.get(sameEntityList.size() - 1).getAttributes().stream()
                    .filter(entityLogAttr -> entityLogAttr.getName().equals(attributeName)).findFirst()
                    .ifPresent(entityLogAttr -> setAttributeNewValue(entityLogAttr, itemToSave));
        }

        Properties properties = new Properties();

        for (EntityLogAttr attr : itemToSave.getAttributes()) {
            properties.setProperty(attr.getName(), attr.getValue());
            if (attr.getValueId() != null) {
                properties.setProperty(attr.getName() + EntityLogAttr.VALUE_ID_SUFFIX, attr.getValueId());
            }
            if (attr.getOldValue() != null) {
                properties.setProperty(attr.getName() + EntityLogAttr.OLD_VALUE_SUFFIX, attr.getOldValue());
            }
            if (attr.getOldValueId() != null) {
                properties.setProperty(attr.getName() + EntityLogAttr.OLD_VALUE_ID_SUFFIX, attr.getOldValueId());
            }
            if (attr.getMessagesPack() != null) {
                properties.setProperty(attr.getName() + EntityLogAttr.MP_SUFFIX, attr.getMessagesPack());
            }
        }

        if (itemToSave.getType() == EntityLogItem.Type.MODIFY) {
            sameEntityList.stream().filter(entityLogItem -> entityLogItem.getType() == EntityLogItem.Type.CREATE)
                    .findFirst().ifPresent(entityLogItem -> itemToSave.setType(EntityLogItem.Type.CREATE));
        }
        itemToSave.setChanges(getChanges(properties));
    }

    protected void setAttributeOldValue(EntityLogAttr entityLogAttr, EntityLogItem itemToSave) {
        EntityLogAttr attr = getAttrToSave(entityLogAttr, itemToSave);
        attr.setOldValue(entityLogAttr.getOldValue());
        attr.setOldValueId(entityLogAttr.getOldValueId());
    }

    protected void setAttributeNewValue(EntityLogAttr entityLogAttr, EntityLogItem itemToSave) {
        EntityLogAttr attr = getAttrToSave(entityLogAttr, itemToSave);
        attr.setValue(entityLogAttr.getValue());
        attr.setValueId(entityLogAttr.getValueId());
    }

    protected EntityLogAttr getAttrToSave(EntityLogAttr entityLogAttr, EntityLogItem itemToSave) {
        EntityLogAttr attr = itemToSave.getAttributes().stream()
                .filter(a -> a.getName().equals(entityLogAttr.getName())).findFirst().orElse(null);
        if (attr == null) {
            attr = metadata.create(EntityLogAttr.class);
            attr.setName(entityLogAttr.getName());
            itemToSave.getAttributes().add(attr);
        }
        return attr;
    }

    protected void saveItem(EntityLogItem item) {
        String storeName = metadataTools.getStoreName(metadata.getClassNN(item.getEntity()));
        if (Stores.isMain(storeName)) {
            EntityManager em = persistence.getEntityManager();
            em.persist(item);
        } else {
            // Create a new transaction in main DB if we are saving an entity from additional data store
            try (Transaction tx = persistence.createTransaction()) {
                EntityManager em = persistence.getEntityManager();
                em.persist(item);
                tx.commit();
            }
        }
    }

    @Override
    public synchronized boolean isEnabled() {
        return config.getEnabled() && isLoggingForCurrentThread();
    }

    @Override
    public synchronized void setEnabled(boolean enabled) {
        if (enabled != config.getEnabled()) {
            config.setEnabled(enabled);
        }
    }

    @Override
    public void invalidateCache() {
        lock.writeLock().lock();
        try {
            log.debug("Invalidating cache");
            entitiesManual = null;
            entitiesAuto = null;
            loaded = false;
        } finally {
            lock.writeLock().unlock();
        }
    }

    protected Set<String> getLoggedAttributes(String entity, boolean auto) {
        lock.readLock().lock();
        try {
            if (!loaded) {
                // upgrade lock
                lock.readLock().unlock();
                lock.writeLock().lock();
                try {
                    if (!loaded) { // recheck because we unlocked for a while
                        loadEntities();
                        loaded = true;
                    }
                } finally {
                    // downgrade lock
                    lock.writeLock().unlock();
                    lock.readLock().lock();
                }
            }

            Set<String> attributes;
            if (auto)
                attributes = entitiesAuto.get(entity);
            else
                attributes = entitiesManual.get(entity);

            return attributes == null ? null : Collections.unmodifiableSet(attributes);
        } finally {
            lock.readLock().unlock();
        }
    }

    protected void loadEntities() {
        log.debug("Loading entities");
        entitiesManual = new HashMap<>();
        entitiesAuto = new HashMap<>();
        Transaction tx = persistence.createTransaction();
        try {
            EntityManager em = persistence.getEntityManager();
            TypedQuery<LoggedEntity> q = em.createQuery(
                    "select e from sec$LoggedEntity e where e.auto = true or e.manual = true", LoggedEntity.class);
            //            q.setView(null);
            List<LoggedEntity> list = q.getResultList();
            for (LoggedEntity loggedEntity : list) {
                if (loggedEntity.getName() == null) {
                    throw new IllegalStateException("Unable to initialize EntityLog: empty LoggedEntity.name");
                }
                Set<String> attributes = new HashSet<>();
                for (LoggedAttribute loggedAttribute : loggedEntity.getAttributes()) {
                    if (loggedAttribute.getName() == null) {
                        throw new IllegalStateException(
                                "Unable to initialize EntityLog: empty LoggedAttribute.name");
                    }
                    attributes.add(loggedAttribute.getName());
                }
                if (BooleanUtils.isTrue(loggedEntity.getAuto()))
                    entitiesAuto.put(loggedEntity.getName(), attributes);
                if (BooleanUtils.isTrue(loggedEntity.getManual()))
                    entitiesManual.put(loggedEntity.getName(), attributes);
            }
            tx.commit();
        } finally {
            tx.end();
        }
        log.debug("Loaded: entitiesAuto={}, entitiesManual={}", entitiesAuto.size(), entitiesManual.size());
    }

    protected String getEntityName(Entity entity) {
        MetaClass metaClass;
        if (entity instanceof CategoryAttributeValue) {
            CategoryAttribute categoryAttribute = ((CategoryAttributeValue) entity).getCategoryAttribute();
            Preconditions.checkNotNullArgument(categoryAttribute, "Category attribute is null");
            metaClass = metadata.getClassNN(categoryAttribute.getCategoryEntityType());
        } else {
            metaClass = metadata.getSession().getClassNN(entity.getClass());
        }
        return metadata.getExtendedEntities().getOriginalOrThisMetaClass(metaClass).getName();
    }

    protected boolean doNotRegister(Entity entity) {
        if (entity == null) {
            return true;
        }
        if (entity instanceof EntityLogItem) {
            return true;
        }
        if (metadata.getTools().hasCompositePrimaryKey(entity.getMetaClass()) && !(entity instanceof HasUuid)) {
            return true;
        }
        return !isEnabled();
    }

    @Override
    public void registerCreate(Entity entity) {
        if (entity == null)
            return;
        registerCreate(entity, false);
    }

    @Override
    public void registerCreate(Entity entity, boolean auto) {
        try {
            if (doNotRegister(entity))
                return;
            String masterEntityName = getEntityName(entity);
            boolean isCategoryAttributeValue = entity instanceof CategoryAttributeValue;

            Set<String> attributes = getLoggedAttributes(masterEntityName, auto);
            if (attributes != null && attributes.contains("*")) {
                attributes = getAllAttributes(entity);
            }
            if (attributes == null) {
                return;
            }

            MetaClass metaClass = metadata.getClassNN(masterEntityName);
            attributes = filterRemovedAttributes(metaClass, attributes);

            if (isCategoryAttributeValue) {
                internalRegisterModifyAttributeValue((CategoryAttributeValue) entity, null, attributes);
            } else {
                String storeName = metadata.getTools().getStoreName(metaClass);
                if (Stores.isMain(storeName)) {
                    internalRegisterCreate(entity, masterEntityName, attributes);
                } else {
                    // Create a new transaction in main DB if we are saving an entity from additional data store
                    try (Transaction tx = persistence.createTransaction()) {
                        internalRegisterCreate(entity, masterEntityName, attributes);
                        tx.commit();
                    }
                }
            }
        } catch (Exception e) {
            logError(entity, e);
        }
    }

    protected Set<String> filterRemovedAttributes(MetaClass metaClass, Set<String> attributes) {
        // filter attributes that do not exists in entity anymore
        return attributes.stream().filter(attributeName -> {
            if (DynamicAttributesUtils.isDynamicAttribute(attributeName)) {
                return DynamicAttributesUtils.getMetaPropertyPath(metaClass, attributeName) != null;
            } else {
                return metaClass.getProperty(attributeName) != null;
            }
        }).collect(Collectors.toSet());
    }

    protected void internalRegisterCreate(Entity entity, String entityName, Set<String> attributes)
            throws IOException {
        Date ts = timeSource.currentTimestamp();
        EntityManager em = persistence.getEntityManager();

        EntityLogItem item = metadata.create(EntityLogItem.class);
        item.setEventTs(ts);
        item.setUser(findUser(em));
        item.setType(EntityLogItem.Type.CREATE);
        item.setEntity(entityName);
        item.setObjectEntityId(referenceToEntitySupport.getReferenceId(entity));
        item.setAttributes(createLogAttributes(entity, attributes, null));

        enqueueItem(item);
    }

    protected void internalRegisterModifyAttributeValue(CategoryAttributeValue entity,
            @Nullable EntityAttributeChanges changes, Set<String> attributes) {
        String propertyName = DynamicAttributesUtils.encodeAttributeCode(entity.getCode());
        if (!attributes.contains(propertyName)) {
            return;
        }

        Date ts = timeSource.currentTimestamp();
        EntityManager em = persistence.getEntityManager();

        Set<String> dirty;
        if (changes == null) {
            dirty = persistence.getTools().getDirtyFields(entity);
        } else {
            dirty = changes.getAttributes();
        }
        boolean registerDeleteOp = dirty.contains("deleteTs") && entity.isDeleted();
        boolean hasChanges = dirty.stream().anyMatch(s -> s.endsWith("Value"));
        if (hasChanges) {
            EntityLogItem item = metadata.create(EntityLogItem.class);
            item.setEventTs(ts);
            item.setUser(findUser(em));
            item.setType(EntityLogItem.Type.MODIFY);
            item.setEntity(getEntityName(entity));
            item.setObjectEntityId(entity.getObjectEntityId());
            item.setAttributes(createDynamicLogAttribute(entity, changes, registerDeleteOp));

            enqueueItem(item);
        }
    }

    protected User findUser(EntityManager em) {
        if (AppContext.isStarted())
            return em.getReference(User.class, userSessionSource.getUserSession().getUser().getId());
        else {
            String login = serverConfig.getJmxUserLogin();
            TypedQuery<User> query = em.createQuery("select u from sec$User u where u.loginLowerCase = ?1",
                    User.class);
            query.setParameter(1, login);
            User user = query.getFirstResult();
            if (user != null)
                return user;
            else
                throw new RuntimeException(
                        "The user '" + login + "' specified in cuba.jmxUserLogin does not exist");
        }
    }

    protected void enqueueItem(EntityLogItem item) {
        EntityManagerContext context = persistence.getEntityManagerContext();
        List<EntityLogItem> items = context.getAttribute(EntityLog.class.getName());
        if (items == null) {
            items = new ArrayList<>();
            context.setAttribute(EntityLog.class.getName(), items);
        }
        items.add(item);
    }

    @Override
    public void registerModify(Entity entity) {
        registerModify(entity, false);
    }

    @Override
    public void registerModify(Entity entity, boolean auto) {
        registerModify(entity, auto, null);
    }

    @Override
    public void registerModify(Entity entity, boolean auto, @Nullable EntityAttributeChanges changes) {
        try {
            if (doNotRegister(entity))
                return;

            String masterEntityName = getEntityName(entity);
            boolean isCategoryAttributeValue = entity instanceof CategoryAttributeValue;
            Set<String> attributes = getLoggedAttributes(masterEntityName, auto);
            if (attributes != null && attributes.contains("*")) {
                attributes = getAllAttributes(entity);
            }
            if (attributes == null) {
                return;
            }

            MetaClass metaClass = metadata.getClassNN(masterEntityName);
            attributes = filterRemovedAttributes(metaClass, attributes);

            if (isCategoryAttributeValue) {
                internalRegisterModifyAttributeValue((CategoryAttributeValue) entity, changes, attributes);
            } else {
                String storeName = metadataTools.getStoreName(metaClass);
                if (Stores.isMain(storeName)) {
                    internalRegisterModify(entity, changes, metaClass, storeName, attributes);
                } else {
                    // Create a new transaction in main DB if we are saving an entity from additional data store
                    try (Transaction tx = persistence.createTransaction()) {
                        internalRegisterModify(entity, changes, metaClass, storeName, attributes);
                        tx.commit();
                    }
                }
            }
        } catch (Exception e) {
            logError(entity, e);
        }
    }

    protected void internalRegisterModify(Entity entity, @Nullable EntityAttributeChanges changes,
            MetaClass metaClass, String storeName, Set<String> attributes) throws IOException {
        Date ts = timeSource.currentTimestamp();
        EntityManager em = persistence.getEntityManager();

        Set<String> dirty;
        if (changes == null) {
            dirty = persistence.getTools().getDirtyFields(entity);
        } else {
            dirty = changes.getAttributes();
        }

        Set<EntityLogAttr> entityLogAttrs;
        EntityLogItem.Type type;
        if (entity instanceof SoftDelete && dirty.contains("deleteTs") && !((SoftDelete) entity).isDeleted()) {
            type = EntityLogItem.Type.RESTORE;
            entityLogAttrs = createLogAttributes(entity, attributes, changes);
        } else {
            type = EntityLogItem.Type.MODIFY;
            Set<String> dirtyAttributes = new HashSet<>();
            for (String attr : attributes) {
                if (dirty.contains(attr)) {
                    dirtyAttributes.add(attr);
                } else if (!Stores.getAdditional().isEmpty()) {
                    String idAttr = metadataTools.getCrossDataStoreReferenceIdProperty(storeName,
                            metaClass.getPropertyNN(attr));
                    if (idAttr != null && dirty.contains(idAttr)) {
                        dirtyAttributes.add(attr);
                    }
                }
            }
            entityLogAttrs = createLogAttributes(entity, dirtyAttributes, changes);
        }
        if (!entityLogAttrs.isEmpty() || type == EntityLogItem.Type.RESTORE) {
            EntityLogItem item = metadata.create(EntityLogItem.class);
            item.setEventTs(ts);
            item.setUser(findUser(em));
            item.setType(type);
            item.setEntity(metaClass.getName());
            item.setObjectEntityId(referenceToEntitySupport.getReferenceId(entity));
            item.setAttributes(entityLogAttrs);

            enqueueItem(item);
        }
    }

    protected Set<EntityLogAttr> createLogAttributes(Entity entity, Set<String> attributes,
            @Nullable EntityAttributeChanges changes) {
        Set<EntityLogAttr> result = new HashSet<>();
        for (String name : attributes) {
            if (DynamicAttributesUtils.isDynamicAttribute(name)) {
                continue;
            }
            EntityLogAttr attr = metadata.create(EntityLogAttr.class);
            attr.setName(name);

            String value = stringify(entity.getValue(name));
            attr.setValue(value);

            Object valueId = getValueId(value);
            if (valueId != null)
                attr.setValueId(valueId.toString());

            if (changes != null) {
                Object oldValue = changes.getOldValue(name);
                attr.setOldValue(stringify(oldValue));
                Object oldValueId = getValueId(oldValue);
                if (oldValueId != null) {
                    attr.setOldValueId(oldValueId.toString());
                }
            }

            MessageTools messageTools = AppBeans.get(MessageTools.NAME);
            String mp = messageTools.inferMessagePack(name, entity);
            if (mp != null)
                attr.setMessagesPack(mp);

            result.add(attr);
        }
        return result;
    }

    protected Set<EntityLogAttr> createDynamicLogAttribute(CategoryAttributeValue entity,
            @Nullable EntityAttributeChanges changes, boolean registerDeleteOp) {
        Set<EntityLogAttr> result = new HashSet<>();
        EntityLogAttr attr = metadata.create(EntityLogAttr.class);
        attr.setName(DynamicAttributesUtils.encodeAttributeCode(entity.getCode()));

        Object value = entity.getValue();
        attr.setValue(stringify(value));

        Object valueId = getValueId(value);
        if (valueId != null)
            attr.setValueId(valueId.toString());

        if (changes != null || registerDeleteOp) {
            Object oldValue = getOldCategoryAttributeValue(entity, changes);
            attr.setOldValue(stringify(oldValue));
            Object oldValueId = getValueId(oldValue);
            if (oldValueId != null) {
                attr.setOldValueId(oldValueId.toString());
            }
        }
        result.add(attr);
        return result;
    }

    protected String getChanges(Properties properties) {
        try {
            StringWriter writer = new StringWriter();
            properties.store(writer, null);
            String changes = writer.toString();
            if (changes.startsWith("#"))
                changes = changes.substring(changes.indexOf("\n") + 1); // cut off comments line
            return changes;
        } catch (IOException e) {
            throw new RuntimeException("Error writing entity log attributes", e);
        }
    }

    @Override
    public void registerDelete(Entity entity) {
        registerDelete(entity, false);
    }

    @Override
    public void registerDelete(Entity entity, boolean auto) {
        try {
            if (doNotRegister(entity))
                return;

            String masterEntityName = getEntityName(entity);
            boolean isCategoryAttributeValue = entity instanceof CategoryAttributeValue;
            Set<String> attributes = getLoggedAttributes(masterEntityName, auto);
            if (attributes != null && attributes.contains("*")) {
                attributes = getAllAttributes(entity);
            }
            if (attributes == null) {
                return;
            }

            MetaClass metaClass = metadata.getClassNN(masterEntityName);
            attributes = filterRemovedAttributes(metaClass, attributes);
            if (isCategoryAttributeValue) {
                internalRegisterModifyAttributeValue((CategoryAttributeValue) entity, null, attributes);
            } else {
                String storeName = metadata.getTools().getStoreName(metaClass);
                if (Stores.isMain(storeName)) {
                    internalRegisterDelete(entity, masterEntityName, attributes);
                } else {
                    // Create a new transaction in main DB if we are saving an entity from additional data store
                    try (Transaction tx = persistence.createTransaction()) {
                        internalRegisterDelete(entity, masterEntityName, attributes);
                        tx.commit();
                    }
                }
            }
        } catch (Exception e) {
            logError(entity, e);
        }
    }

    protected void internalRegisterDelete(Entity entity, String entityName, Set<String> attributes)
            throws IOException {
        Date ts = timeSource.currentTimestamp();
        EntityManager em = persistence.getEntityManager();

        EntityLogItem item = metadata.create(EntityLogItem.class);
        item.setEventTs(ts);
        item.setUser(findUser(em));
        item.setType(EntityLogItem.Type.DELETE);
        item.setEntity(entityName);
        item.setObjectEntityId(referenceToEntitySupport.getReferenceId(entity));
        item.setAttributes(createLogAttributes(entity, attributes, null));

        enqueueItem(item);
    }

    protected Set<String> getAllAttributes(Entity entity) {
        if (entity == null) {
            return null;
        }
        Set<String> attributes = new HashSet<>();
        MetaClass metaClass = metadata.getClassNN(entity.getClass());
        for (MetaProperty metaProperty : metaClass.getProperties()) {
            Range range = metaProperty.getRange();
            if (range.isClass() && range.getCardinality().isMany()) {
                continue;
            }
            attributes.add(metaProperty.getName());
        }
        Collection<CategoryAttribute> categoryAttributes = dynamicAttributes.getAttributesForMetaClass(metaClass);
        if (categoryAttributes != null) {
            for (CategoryAttribute categoryAttribute : categoryAttributes) {
                if (BooleanUtils.isNotTrue(categoryAttribute.getIsCollection())) {
                    attributes.add(DynamicAttributesUtils.getMetaPropertyPath(metaClass, categoryAttribute)
                            .getMetaProperty().getName());
                }
            }
        }
        return attributes;
    }

    protected Object getValueId(Object value) {
        if (value instanceof EmbeddableEntity) {
            return null;
        } else if (value instanceof BaseGenericIdEntity) {
            return referenceToEntitySupport.getReferenceId((Entity) value);
        } else {
            return null;
        }
    }

    protected String stringify(Object value) {
        if (value == null)
            return "";
        else if (value instanceof Instance) {
            return ((Instance) value).getInstanceName();
        } else if (value instanceof Date) {
            return Datatypes.getNN(value.getClass()).format(value);
        } else if (value instanceof Iterable) {
            StringBuilder sb = new StringBuilder();
            sb.append("[");
            for (Object obj : (Iterable) value) {
                sb.append(stringify(obj)).append(",");
            }
            if (sb.length() > 1)
                sb.deleteCharAt(sb.length() - 1);
            sb.append("]");
            return sb.toString();
        } else {
            return String.valueOf(value);
        }
    }

    protected Object getOldCategoryAttributeValue(CategoryAttributeValue attributeValue,
            EntityAttributeChanges changes) {
        CategoryAttribute categoryAttribute = attributeValue.getCategoryAttribute();
        PersistenceTools persistenceTools = persistence.getTools();
        String fieldName = null;
        switch (categoryAttribute.getDataType()) {
        case DATE:
            fieldName = "dateValue";
            break;
        case ENUMERATION:
        case STRING:
            fieldName = "stringValue";
            break;
        case INTEGER:
            fieldName = "intValue";
            break;
        case DOUBLE:
            fieldName = "doubleValue";
            break;
        case BOOLEAN:
            fieldName = "booleanValue";
            break;
        }
        if (fieldName != null) {
            return changes != null ? changes.getOldValue(fieldName)
                    : persistenceTools.getOldValue(attributeValue, fieldName);
        }
        return null;
    }

    protected void logError(Entity entity, Exception e) {
        log.warn("Unable to log entity {}, id={}", entity, entity.getId(), e);
    }
}