/*
 * The Kuali Financial System, a comprehensive financial management system for higher education.
 *
 * Copyright 2005-2016 The Kuali Foundation
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program 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 Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.kuali.kfs.sys.context;

import org.apache.commons.lang.StringUtils;
import org.kuali.kfs.krad.datadictionary.DataDictionary;
import org.kuali.kfs.krad.datadictionary.DocumentEntry;
import org.kuali.kfs.krad.service.DataDictionaryService;
import org.kuali.kfs.sys.ConfigureContext;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

@ConfigureContext
/**
 * Tests that every property on all KFS documents are either serializable or transient
 */
public class DocumentSerializabilityTest extends KualiTestBase {
    private static org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(DocumentSerializabilityTest.class);

    /**
     * Tests that every property on all KFS documents are either serializable or transient
     */
    public void testAllDocumentsAreSerializable() {
        DataDictionary dataDictionary = SpringContext.getBean(DataDictionaryService.class).getDataDictionary();
        Map<String, DocumentEntry> documentEntries = dataDictionary.getDocumentEntries();
        Set<Class<?>> checkedClasses = new HashSet<Class<?>>();
        Set<Class<?>> unserializableClasses = new HashSet<Class<?>>();
        String errorMessage = "";

        for (String documentEntryName : documentEntries.keySet()) {
            if (LOG.isDebugEnabled()) {
                LOG.debug("Checking: " + documentEntryName);
            }
            DocumentEntry entry = documentEntries.get(documentEntryName);

            Set<String> unserializableFields = null;
            Class<?> testedClass;
            if (entry instanceof org.kuali.kfs.kns.datadictionary.MaintenanceDocumentEntry) {
                testedClass = ((org.kuali.kfs.kns.datadictionary.MaintenanceDocumentEntry) entry).getMaintainableClass();
                if (testedClass == null) {
                    errorMessage += "Maintenance Document entry: " + documentEntryName + " has a null maintainable class";
                } else {
                    unserializableFields = getUnserializableFieldsFromClass(testedClass);
                }
            } else if (entry instanceof org.kuali.kfs.kns.datadictionary.TransactionalDocumentEntry) {
                testedClass = ((org.kuali.kfs.kns.datadictionary.TransactionalDocumentEntry) entry).getDocumentClass();
                if (testedClass == null) {
                    errorMessage += "Transactional Document entry: " + documentEntryName + " has a null document class";
                } else {
                    unserializableFields = getUnserializableFieldsFromClass(testedClass);
                }
            } else {
                unserializableFields = null;
                testedClass = null;
            }

            if (testedClass != null && !checkedClasses.contains(testedClass)) {
                checkedClasses.add(testedClass);
                if (unserializableFields != null && !unserializableFields.isEmpty()) {
                    errorMessage += "Class " + testedClass.getName() + " has unserializable fields: " + StringUtils.join(unserializableFields, ',') + "\n";
                    unserializableClasses.add(testedClass);
                }
            }
        }

        if (!StringUtils.isBlank(errorMessage)) {
            LOG.info(errorMessage);
        }
        assertTrue(errorMessage, StringUtils.isEmpty(errorMessage));
    }

    /**
     * Determines if any of the fields of the given class are unserializable
     *
     * @param clazz the class to check
     * @return a Set of field names, the unserializable field names
     */
    protected Set<String> getUnserializableFieldsFromClass(Class<?> clazz) {
        Set<String> unserializableFields = new HashSet<String>();

        if (clazz.getSuperclass() != null && !clazz.equals(java.lang.Object.class)) {
            unserializableFields.addAll(getUnserializableFieldsFromClass(clazz.getSuperclass()));
        }
        Field[] fields = clazz.getDeclaredFields();
        for (Field field : fields) {
            if (!isSerializable(field)) {
                unserializableFields.add(field.getName());
            }
        }

        return unserializableFields;
    }

    /**
     * Determines if a given field is serializable
     *
     * @param field the field to check
     * @return true if the field is serializable, false otherwise
     */
    protected boolean isSerializable(Field field) {
        return java.io.Serializable.class.isAssignableFrom(field.getType())
            || java.util.Collection.class.isAssignableFrom(field.getType())
            || java.util.Map.class.isAssignableFrom(field.getType())
            || field.getName().equals("dataObject")
            || Modifier.isTransient(field.getModifiers())
            || Modifier.isStatic(field.getModifiers())
            || isPrimitive(field.getType());
    }

    /**
     * Determines if the given class represents one of the eight Java primitives
     *
     * @param clazz the class to check
     * @return true if the class represents a byte, short, int, long, char, double, float, or boolean; false otherwise
     */
    protected boolean isPrimitive(Class<?> clazz) {
        return Byte.TYPE.isAssignableFrom(clazz) || Short.TYPE.isAssignableFrom(clazz) || Integer.TYPE.isAssignableFrom(clazz) || Long.TYPE.isAssignableFrom(clazz) || Float.TYPE.isAssignableFrom(clazz) || Double.TYPE.isAssignableFrom(clazz) || Character.TYPE.isAssignableFrom(clazz) || Boolean.TYPE.isAssignableFrom(clazz);
    }
}
