AuditEntityListener.java

package sk.iway.iwcm.system.adminlog;

import org.eclipse.persistence.jpa.JpaEntityManager;
import org.eclipse.persistence.jpa.JpaHelper;
import org.springframework.beans.BeanWrapperImpl;
import org.springframework.beans.NotReadablePropertyException;
import org.springframework.core.annotation.AnnotationUtils;
import sk.iway.iwcm.Adminlog;
import sk.iway.iwcm.Constants;
import sk.iway.iwcm.DBPool;
import sk.iway.iwcm.Logger;
import sk.iway.iwcm.RequestBean;
import sk.iway.iwcm.SetCharacterEncodingFilter;
import sk.iway.iwcm.Tools;
import sk.iway.iwcm.database.DataSource;
import sk.iway.iwcm.doc.DocDB;
import sk.iway.iwcm.doc.PerexGroupBean;

import javax.persistence.*;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.stream.Stream;

/**
 * Listener auditujuci zmeny v JPA entitach. Pouziva sa nastavenim anotacii na JPA entite:
 *
 * @EntityListeners(sk.iway.iwcm.system.adminlog.AuditEntityListener.class)
 * @EntityListenersType(sk.iway.iwcm.Adminlog.TYPE_GALLERY)
 *
 * viac je v dokumentacii v auditing.md
 * ticket: #46891
 */
public class AuditEntityListener {

    private HashMap<Long, String> preUpdateChanges;

    @PrePersist
    public void prePersist(Object entity) {
        //log.debug("prePersist");
    }

    @PreUpdate
    public void preUpdate(Object entity) {
        if (isDisabled(entity)) return;
        try {
            saveChanges(entity);
        } catch (Exception ex) {
            sk.iway.iwcm.Logger.error(ex);
        }
    }

    @PreRemove
    public void preRemove(Object entity) {
        //log.debug("preRemove: " + getId(entity));
    }

    @PostPersist
    public void postPersist(Object entity) {
        if (isDisabled(entity)) return;
        try {
            //log.debug("postPersist");
            String changes = getChangedProperties(entity, false);
            Number id = getId(entity);
            Adminlog.add(getType(entity), "CREATE:\n" + changes, id.intValue(), -1);
        } catch (Exception ex) {
            sk.iway.iwcm.Logger.error(ex);
        }
    }

    @PostUpdate
    public void postUpdate(Object entity) {
        if (isDisabled(entity)) return;
        try {
            Logger.debug(getClass(), "postUpdate");
            String changes = getAndClearSavedChanges(entity);
            Number id = getId(entity);
            Logger.debug(getClass(), "postUpdate: id: " + id + ", changeds: " + changes);
            Adminlog.add(getType(entity), "UPDATE:\nid: " + id.longValue() + "\n" + changes, id.intValue(), -1);
        } catch (Exception ex) {
            sk.iway.iwcm.Logger.error(ex);
        }
    }

    @PostRemove
    public void postRemove(Object entity) {
        if (isDisabled(entity)) return;
        try {
            //log.debug("postRemove");
            Number id = getId(entity);
            String changes = getChangedProperties(entity, false);
            Adminlog.add(getType(entity), "DELETE:\nid: " + id.longValue() +  "\n" + changes, id.intValue(), -1);
        } catch (Exception ex) {
            sk.iway.iwcm.Logger.error(ex);
        }
    }

    @PostLoad
    public void postLoad(Object entity) {
        //nothing here
    }

    /**
     * Overi, ci pre danu entitu je povoleny auditing
     * @param entity
     * @return
     */
    private boolean isDisabled(Object entity) {
        String auditJpaDisabledEntities = Constants.getString("auditJpaDisabledEntities");
        if ("*".equals(auditJpaDisabledEntities)) return true;
        if (Tools.isEmpty(auditJpaDisabledEntities)) return false;

        String fqdn = entity.getClass().getName();
        if (auditJpaDisabledEntities.contains(fqdn)) return true;

        return false;
    }

    /**
     * Ziska typ auditneho zaznamu podla anotacie @EntityListenersType(sk.iway.iwcm.Adminlog.TYPE_GALLERY)
     * @param entity
     * @return
     */
    private int getType(Object entity) {
        int result = Adminlog.TYPE_CLIENT_SPECIFIC;
        EntityListenersType annotation = AnnotationUtils.findAnnotation(entity.getClass(), EntityListenersType.class);
        if (annotation != null && annotation.value() > 0) {
            result = annotation.value();
        }

        return result;
    }

    /**
     * Ziska primarny kluc z entity
     * @param entity
     * @return
     */
    private Number getId(Object entity) {
        try {
            BeanWrapperImpl entityWrapper = new BeanWrapperImpl(entity);
            Object id = entityWrapper.getPropertyValue("id");
            if (id instanceof Number) return (Number)id;
            return 0;
        } catch (NotReadablePropertyException ex) {
            //neexistuje property s menom id
        }

        //skus najst property podla anotacie @Id
        try {
            Field[] declaredFields = getDeclaredFieldsTwoLevels(entity.getClass());
            for (Field declaredField : declaredFields) {
                if (declaredField.isAnnotationPresent(Id.class)) {
                    BeanWrapperImpl entityWrapper = new BeanWrapperImpl(entity);
                    Object id = entityWrapper.getPropertyValue(declaredField.getName());
                    if (id instanceof Number) return (Number)id;
                }
            }
        } catch (Exception ex) {
            sk.iway.iwcm.Logger.error(ex);
        }

        //lepsie vratit 0 ako Exception, aspon sa zaaudituje nieco
        return 0;
    }


    /**
     * chcel by som, aby to zauditovalo aj zoznam zmien, volakedy ked sme pouzivali Cayenne sme mali metodu, ktorej sme dali zmeneny bean, ten si fetchol podla IDecka aktualny z DB, preiteroval property a zmeny pekne vypisal na kazdy riadok s menom property.
     * @param entity
     * @param compareToDb - ak je true nacita sa aktualna entita z DB a spravi sa porovnanie
     * @return
     */
    private String getChangedProperties(Object entity, boolean compareToDb) {
        BeanWrapperImpl entityWrapper = new BeanWrapperImpl(entity);

        Number id = getId(entity);
        StringBuilder log = new StringBuilder();

        //add auditValues from RequestBean
        RequestBean requestBean = SetCharacterEncodingFilter.getCurrentRequestBean();
        if (requestBean != null) {
            log.append(Adminlog.getRequestBeanAuditLog(requestBean));
        }

        List<String> auditHideProperties = Tools.getStringListValue(Tools.getTokens(Constants.getString("auditHideProperties", ""), ","));

        Object dbEntity = null;
        if (compareToDb && id != null && id.longValue() > 0) {
            try {
                //musime otvotit novy entitymanager aby sme ziskali aktualne data v DB
                String dbName = "iwcm";
                Class<?> clazz = entity.getClass();
                if (clazz.isAnnotationPresent(DataSource.class)) {

                    Annotation annotation = clazz.getAnnotation(DataSource.class);
                    DataSource dataSource = (DataSource) annotation;
                    dbName = dataSource.name();
                }
                EntityManagerFactory factory = DBPool.getEntityManagerFactory(dbName);
                //factory.getProperties().put("eclipselink.session.customizer", "sk.iway.webjet.v9.JpaSessionCustomizer");

                JpaEntityManager em = JpaHelper.getEntityManager(factory.createEntityManager());
                dbEntity = em.find(entity.getClass(), id);
                em.close();
            } catch (Exception ex) {
                sk.iway.iwcm.Logger.error(ex);
            }
        }

        BeanWrapperImpl dbEntityWrapper = null;
        if (dbEntity!=null) dbEntityWrapper = new BeanWrapperImpl(dbEntity);

        Field[] declaredFields = getDeclaredFieldsTwoLevels(entity.getClass());
        for (Field declaredField : declaredFields) {
            try {
                String name = declaredField.getName();
                if ("serialVersionUID".equals(name)) continue;
                if ("dataAsc".equals(name) || "data_asc".equals(name)) continue;

                String entityValue = getStringValue(entityWrapper.getPropertyValue(name), name);
                String dbEntityValue = null;
                if (dbEntityWrapper!=null) dbEntityValue = getStringValue(dbEntityWrapper.getPropertyValue(name), name);

                //key mame kvoli TranslationKeys
                if (compareToDb==false || !entityValue.equals(dbEntityValue) || "key".equals(name)) {
                    // Tu by sme to este vedeli vylepsit, pretoze z anotacii aj vieme ziskat pekne meno ako sa pouziva v datatabulke.
                    Column annotation = AnnotationUtils.getAnnotation(declaredField, Column.class);
                    String columnName = annotation != null ? annotation.name() : name;
                    String value;
                    if (dbEntity!=null) value = dbEntityValue + " -> " + entityValue;
                    else value = entityValue;

                    if (auditHideProperties.contains(name) || auditHideProperties.contains(columnName)) {
                        value = "*****";
                    }

                    if (log.length()>0) log.append("\n");

                    log.append(columnName).append(": ");
                    log.append(value);
                }
            } catch (Exception ex) {
                //sk.iway.iwcm.Logger.error(ex);
            }
        }
        return log.toString();
    }

    /**
     * Format Object propertyValue to String representation
     * @param entityPropertyValue
     * @return
     */
    private String getStringValue(Object entityPropertyValue, String name) {
        String entityValue = null;
        if (entityPropertyValue == null) {
            entityValue = "null";
        } else {
            if (entityPropertyValue.getClass().isArray()) {
                Object[] array = (Object[]) entityPropertyValue;
                StringBuilder builder = new StringBuilder();
                for (Object o : array) {
                    if (builder.isEmpty()==false) builder.append(",");

                    if (name.equals("perexGroups") && o instanceof Integer) {
                        Integer i = (Integer) o;
                        PerexGroupBean pgb = DocDB.getInstance().getPerexGroup(i, null);
                        if (pgb.getPerexGroupId()>0) builder.append(pgb.getPerexGroupNameId());
                        else builder.append(i);
                    } else {
                        builder.append(String.valueOf(o));
                    }
                }
                entityValue = builder.toString();
            } else if (entityPropertyValue instanceof Date) {
                entityValue = Tools.formatDateTimeSeconds((Date) entityPropertyValue);
            } else if (entityPropertyValue instanceof Long && name.contains("date")) {
                entityValue = Tools.formatDateTimeSeconds((Long) entityPropertyValue);
            } else {
                entityValue = shrinkValue(String.valueOf(entityPropertyValue));
            }
        }
        return shrinkValue(entityValue);
    }

    /**
     * Skrati hodnotu, aby sa neauditovali dlhe zaznamy a audit nezaberal vela miesta
     * @param value
     * @return
     */
    private String shrinkValue(String value) {
        if (value == null || "null".equals(value)) return "null";
        int maxLength = Constants.getInt("auditMaxChangeLength", 100);
        if (value.length()>maxLength) value = value.substring(0, maxLength-1)+"...";
        return value;
    }

    /**
     * Ziska zoznam zmien entity v databaze voci entite ako parameter
     * zmenu ulozi do mapy preUpdateChanges pre neskorsie ziskanie
     * @param entity - aktualna entita, ktoru ideme ukladat
     */
    private void saveChanges(Object entity) {
        Number id = getId(entity);
        if (id.longValue() > 0) {
            String changes = getChangedProperties(entity, true);
            if (preUpdateChanges == null) {
                preUpdateChanges = new HashMap<>();
            }
            preUpdateChanges.put(id.longValue(), changes);
        }
    }

    /**
     * Vrati ulozeny zoznam zmien z hashmapy a zmaze ho z hashmapy (aby sa zbytocne nezaplnala pamat)
     * @param entity
     * @return
     */
    private String getAndClearSavedChanges(Object entity) {
        Number id = getId(entity);
        if (id.longValue() > 0 && preUpdateChanges != null) {
            String changes = preUpdateChanges.get(id.longValue());
            if (changes!=null) {
                preUpdateChanges.remove(id);
                return changes;
            }
        }
        return "";
    }

    /**
     * Returns o.getDeclaredFields also from extended object, for use in DataTables (we are traversing max to one level parent)
     * @param o
     * @return
     */
    public static Field[] getDeclaredFieldsTwoLevels(Class<?> o) {
        Field[] declaredFields;
        if(o.getSuperclass() != null) {
            declaredFields = Stream.concat(Arrays.stream(o.getDeclaredFields()), Arrays.stream(o.getSuperclass().getDeclaredFields())).toArray(Field[]::new);
        } else {
            declaredFields = o.getDeclaredFields();
        }
        return declaredFields;
    }
}