/*
 * 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.krad.util;


import org.apache.commons.lang.StringEscapeUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.builder.EqualsBuilder;
import org.apache.commons.lang.builder.HashCodeBuilder;
import org.apache.commons.lang.builder.ToStringBuilder;
import org.springframework.util.AutoPopulatingList;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;


/**
 * Holds errors due to validation. Keys of map represent property paths, and value is a AutoPopulatingList that contains resource string
 * keys (to retrieve the error message).
 * <p>
 * Note, prior to rice 0.9.4, this class implemented {@link java.util.Map}.  The implements has been removed as of rice 0.9.4
 */
public class MessageMap implements Serializable {

    private static final long serialVersionUID = -2328635367656516150L;

    private List<String> errorPath = new ArrayList<String>();
    private Map<String, AutoPopulatingList<ErrorMessage>> errorMessages = new LinkedHashMap<String, AutoPopulatingList<ErrorMessage>>();
    private Map<String, AutoPopulatingList<ErrorMessage>> warningMessages = new LinkedHashMap<String, AutoPopulatingList<ErrorMessage>>();
    private Map<String, AutoPopulatingList<ErrorMessage>> infoMessages = new LinkedHashMap<String, AutoPopulatingList<ErrorMessage>>();

    public MessageMap() {
    }

    public MessageMap(MessageMap messageMap) {
        this.errorPath = messageMap.errorPath;
        this.errorMessages = messageMap.errorMessages;
        this.warningMessages = messageMap.warningMessages;
        this.infoMessages = messageMap.infoMessages;
    }


    public void merge(MessageMap messageMap) {
        if (messageMap != null) {
            if (messageMap.hasErrors()) {
                merge(messageMap.getErrorMessages(), errorMessages);
            }
            if (messageMap.hasInfo()) {
                merge(messageMap.getInfoMessages(), infoMessages);
            }
            if (messageMap.hasWarnings()) {
                merge(messageMap.getWarningMessages(), warningMessages);
            }

        }

    }

    /**
     * This method takes one message map and merges it into another.  Makes sure there are no duplicates.
     *
     * @param messagesFrom
     * @param messagesTo
     */
    protected void merge(Map<String, AutoPopulatingList<ErrorMessage>> messagesFrom, Map<String, AutoPopulatingList<ErrorMessage>> messagesTo) {
        for (String key : messagesFrom.keySet()) {

            if (messagesTo.containsKey(key)) {
                // now we need to merge the messages
                AutoPopulatingList<ErrorMessage> tal = messagesFrom.get(key);
                AutoPopulatingList<ErrorMessage> parentList = messagesTo.get(key);

                for (Object o : tal) {

                    if (!parentList.contains(o)) {
                        parentList.add((ErrorMessage) o);
                    }
                }

            } else {
                messagesTo.put(key, messagesFrom.get(key));
            }

        }

    }

    /**
     * Adds an error to the map under the given propertyName and adds an array of message parameters. This will fully prepend the
     * propertyName with the current errorPath.
     *
     * @param propertyName    name of the property to add error under
     * @param errorKey        resource key used to retrieve the error text from the error message resource bundle
     * @param errorParameters zero or more string parameters for the displayed error message
     * @return AutoPopulatingList
     */
    public AutoPopulatingList<ErrorMessage> putError(String propertyName, String errorKey, String... errorParameters) {
        return putMessageInMap(errorMessages, propertyName, errorKey, true, true, errorParameters);
    }

    public AutoPopulatingList<ErrorMessage> putWarning(String propertyName, String messageKey, String... messageParameters) {
        return putMessageInMap(warningMessages, propertyName, messageKey, true, true, messageParameters);
    }

    public AutoPopulatingList<ErrorMessage> putInfo(String propertyName, String messageKey, String... messageParameters) {
        return putMessageInMap(infoMessages, propertyName, messageKey, true, true, messageParameters);
    }

    /**
     * Adds an error to the map under the given propertyName and adds an array of message parameters. This will
     * <strong>not</strong> prepend the propertyName with the current errorPath.
     *
     * @param propertyName    name of the property to add error under
     * @param errorKey        resource key used to retrieve the error text from the error message resource bundle
     * @param errorParameters zero or more string parameters for the displayed error message
     * @return AutoPopulatingList
     */
    public AutoPopulatingList<ErrorMessage> putErrorWithoutFullErrorPath(String propertyName, String errorKey, String... errorParameters) {
        return putMessageInMap(errorMessages, propertyName, errorKey, false, true, errorParameters);
    }

    public AutoPopulatingList<ErrorMessage> putWarningWithoutFullErrorPath(String propertyName, String messageKey, String... messageParameters) {
        return putMessageInMap(warningMessages, propertyName, messageKey, false, true, messageParameters);
    }

    public AutoPopulatingList<ErrorMessage> putInfoWithoutFullErrorPath(String propertyName, String messageKey, String... messageParameters) {
        return putMessageInMap(infoMessages, propertyName, messageKey, false, true, messageParameters);
    }

    /**
     * Adds an error related to a particular section identified by its section ID.  For maintenance documents, the section ID is identified
     * by calling {@link org.kuali.rice.kns.datadictionary.MaintainableSectionDefinition#getId()}
     *
     * @param sectionId
     * @param errorKey
     * @param errorParameters
     * @return
     */
    public AutoPopulatingList<ErrorMessage> putErrorForSectionId(String sectionId, String errorKey, String... errorParameters) {
        return putErrorWithoutFullErrorPath(sectionId, errorKey, errorParameters);
    }

    public AutoPopulatingList<ErrorMessage> putWarningForSectionId(String sectionId, String messageKey, String... messageParameters) {
        return putWarningWithoutFullErrorPath(sectionId, messageKey, messageParameters);
    }

    public AutoPopulatingList<ErrorMessage> putInfoForSectionId(String sectionId, String messageKey, String... messageParameters) {
        return putInfoWithoutFullErrorPath(sectionId, messageKey, messageParameters);
    }

    /**
     * adds an error to the map under the given propertyName and adds an array of message parameters.
     *
     * @param propertyName                name of the property to add error under
     * @param messageKey                  resource key used to retrieve the error text from the error message resource bundle
     * @param prependFullErrorPath        true if you want the whole parent error path prepended, false otherwise
     * @param escapeHtmlMessageParameters whether to escape HTML characters in the message parameters, provides protection against XSS attacks
     * @param messageParameters           zero or more string parameters for the displayed error message
     * @return TypeArrayList
     */
    private AutoPopulatingList<ErrorMessage> putMessageInMap(Map<String, AutoPopulatingList<ErrorMessage>> messagesMap, String propertyName, String messageKey, boolean prependFullErrorPath, boolean escapeHtmlMessageParameters, String... messageParameters) {
        if (StringUtils.isBlank(propertyName)) {
            throw new IllegalArgumentException("invalid (blank) propertyName");
        }
        if (StringUtils.isBlank(messageKey)) {
            throw new IllegalArgumentException("invalid (blank) errorKey");
        }

        // check if we have previous errors for this property
        AutoPopulatingList<ErrorMessage> errorList = null;
        String propertyKey = getKeyPath(propertyName, prependFullErrorPath);
        if (messagesMap.containsKey(propertyKey)) {
            errorList = messagesMap.get(propertyKey);
        } else {
            errorList = new AutoPopulatingList<ErrorMessage>(ErrorMessage.class);
        }

        if (escapeHtmlMessageParameters && messageParameters != null) {
            String[] filteredMessageParameters = new String[messageParameters.length];
            for (int i = 0; i < messageParameters.length; i++) {
                filteredMessageParameters[i] = StringEscapeUtils.escapeHtml(messageParameters[i]);
            }
            messageParameters = filteredMessageParameters;
        }

        // add error to list
        ErrorMessage errorMessage = new ErrorMessage(messageKey, messageParameters);
        // check if this error has already been added to the list
        if (!errorList.contains(errorMessage)) {
            errorList.add(errorMessage);
        }

        return messagesMap.put(propertyKey, errorList);
    }

    /**
     * If any error messages with the key targetKey exist in this ErrorMap for the named property, those ErrorMessages will be
     * replaced with a new ErrorMessage with the given replaceKey and replaceParameters.
     *
     * @param propertyName      name of the property where existing error will be replaced
     * @param targetKey         error key of message to be replaced
     * @param replaceParameters zero or more string parameters for the replacement error message
     * @return true if the replacement occurred
     * @paran replaceKey error key which will replace targetKey
     */
    public boolean replaceError(String propertyName, String targetKey, String replaceKey, String... replaceParameters) {
        return replaceError(propertyName, targetKey, true, replaceKey, replaceParameters);
    }

    /**
     * If any error messages with the key targetKey exist in this ErrorMap for the named property, those ErrorMessages will be
     * replaced with a new ErrorMessage with the given replaceKey and replaceParameters. The targetKey and replaceKey will be
     * prepended with the current errorPath, if any.
     *
     * @param propertyName      name of the property where existing error will be replaced
     * @param targetKey         error key of message to be replaced
     * @param replaceParameters zero or more string parameters for the replacement error message
     * @return true if the replacement occurred
     * @paran replaceKey error key which will replace targetKey
     */
    public boolean replaceErrorWithoutFullErrorPath(String propertyName, String targetKey, String replaceKey, String... replaceParameters) {
        return replaceError(propertyName, targetKey, false, replaceKey, replaceParameters);
    }


    /**
     * If any error messages with the key targetKey exist in this ErrorMap for the named property, those ErrorMessages will be
     * replaced with a new ErrorMessage with the given replaceKey and replaceParameters.
     *
     * @param propertyName      name of the property to add error under
     * @param targetKey         resource key used to retrieve the error text
     * @param withFullErrorPath true if you want the whole parent error path appended, false otherwise
     * @param replaceParameters zero or more string parameters for the displayed error message
     * @return true if the replacement occurred
     */
    private boolean replaceError(String propertyName, String targetKey, boolean withFullErrorPath, String replaceKey, String... replaceParameters) {
        boolean replaced = false;

        if (StringUtils.isBlank(propertyName)) {
            throw new IllegalArgumentException("invalid (blank) propertyName");
        }
        if (StringUtils.isBlank(targetKey)) {
            throw new IllegalArgumentException("invalid (blank) targetKey");
        }
        if (StringUtils.isBlank(replaceKey)) {
            throw new IllegalArgumentException("invalid (blank) replaceKey");
        }

        // check if we have previous errors for this property
        AutoPopulatingList<ErrorMessage> errorList = null;
        String propertyKey = getKeyPath(propertyName, withFullErrorPath);
        if (errorMessages.containsKey(propertyKey)) {
            errorList = errorMessages.get(propertyKey);

            // look for the specific targetKey
            for (int i = 0; i < errorList.size(); ++i) {
                ErrorMessage em = errorList.get(i);

                // replace matching messages
                if (em.getErrorKey().equals(targetKey)) {
                    ErrorMessage rm = new ErrorMessage(replaceKey, replaceParameters);
                    errorList.set(i, rm);
                    replaced = true;
                }
            }
        }

        return replaced;
    }


    /**
     * Returns true if the named field has a message with the given errorKey
     *
     * @param errorKey
     * @param fieldName
     * @return boolean
     */
    public boolean fieldHasMessage(String fieldName, String errorKey) {
        boolean found = false;

        List<ErrorMessage> fieldMessages = errorMessages.get(fieldName);
        if (fieldMessages != null) {
            for (Iterator<ErrorMessage> i = fieldMessages.iterator(); !found && i.hasNext(); ) {
                ErrorMessage errorMessage = i.next();
                found = errorMessage.getErrorKey().equals(errorKey);
            }
        }

        return found;
    }

    /**
     * Returns the number of messages for the given field
     *
     * @param fieldName
     * @return int
     */
    public int countFieldMessages(String fieldName) {
        int count = 0;

        List<ErrorMessage> fieldMessages = errorMessages.get(fieldName);
        if (fieldMessages != null) {
            count = fieldMessages.size();
        }

        return count;
    }


    /**
     * @return true if the given messageKey is associated with some property in this ErrorMap
     */
    public boolean containsMessageKey(String messageKey) {
        ErrorMessage foundMessage = null;

        if (!hasNoErrors()) {
            for (Iterator<Map.Entry<String, AutoPopulatingList<ErrorMessage>>> i = getAllPropertiesAndErrors().iterator(); (foundMessage == null) && i.hasNext(); ) {
                Map.Entry<String, AutoPopulatingList<ErrorMessage>> e = i.next();
                AutoPopulatingList<ErrorMessage> entryErrorList = e.getValue();
                for (Iterator<ErrorMessage> j = entryErrorList.iterator(); j.hasNext(); ) {
                    ErrorMessage em = j.next();
                    if (messageKey.equals(em.getErrorKey())) {
                        foundMessage = em;
                    }
                }
            }
        }

        return (foundMessage != null);
    }


    private int getMessageCount(Map<String, AutoPopulatingList<ErrorMessage>> messageMap) {
        int messageCount = 0;
        for (Iterator<String> iter = messageMap.keySet().iterator(); iter.hasNext(); ) {
            String errorKey = iter.next();
            List<ErrorMessage> errors = messageMap.get(errorKey);
            messageCount += errors.size();
        }

        return messageCount;
    }

    /**
     * Counts the total number of error messages in the map
     *
     * @return returns an int for the total number of errors
     */
    public int getErrorCount() {
        return getMessageCount(errorMessages);
    }

    /**
     * Counts the total number of warning messages in the map
     *
     * @return returns an int for the total number of warnings
     */
    public int getWarningCount() {
        return getMessageCount(warningMessages);
    }

    /**
     * Counts the total number of info messages in the map
     *
     * @return returns an int for the total number of info
     */
    public int getInfoCount() {
        return getMessageCount(infoMessages);
    }

    /**
     * @param path
     * @return Returns a List of ErrorMessages for the given path
     */
    public AutoPopulatingList<ErrorMessage> getMessages(String path) {
        return errorMessages.get(path);
    }

    /**
     * Adds a string prefix to the error path.
     *
     * @param parentName
     */
    public void addToErrorPath(String parentName) {
        errorPath.add(parentName);
    }

    /**
     * This method returns the list that holds the error path values.
     *
     * @return List
     */
    public List<String> getErrorPath() {
        return errorPath;
    }

    /**
     * Removes a string prefix from the error path.
     *
     * @param parentName
     * @return boolean Returns true if the parentName existed, false otherwise.
     */
    public boolean removeFromErrorPath(String parentName) {
        return errorPath.remove(parentName);
    }

    /**
     * Clears the errorPath.
     */
    public void clearErrorPath() {
        errorPath.clear();
    }

    /**
     * This is what's prepended to the beginning of the key. This is built by iterating over all of the entries in the errorPath
     * list and concatenating them together with a "."
     *
     * @param propertyName
     * @param prependFullErrorPath
     * @return String Returns the keyPath.
     */
    public String getKeyPath(String propertyName, boolean prependFullErrorPath) {
        String keyPath = "";

        if (KRADConstants.GLOBAL_ERRORS.equals(propertyName)) {
            return KRADConstants.GLOBAL_ERRORS;
        }

        if (!errorPath.isEmpty() && prependFullErrorPath) {
            keyPath = StringUtils.join(errorPath.iterator(), ".");
            keyPath += (keyPath != null && keyPath.endsWith(".")) ? propertyName : "." + propertyName;
        } else {
            keyPath = propertyName;
        }

        return keyPath;
    }

    /**
     * @return List of the property names that have errors.
     */
    public List<String> getPropertiesWithErrors() {
        List<String> properties = new ArrayList<String>();

        for (Iterator<String> iter = errorMessages.keySet().iterator(); iter.hasNext(); ) {
            properties.add(iter.next());
        }

        return properties;
    }

    /**
     * @return List of the property names that have warnings.
     */
    public List<String> getPropertiesWithWarnings() {
        List<String> properties = new ArrayList<String>(warningMessages.keySet());
        return properties;
    }

    /**
     * @return List of the property names that have info.
     */
    public List<String> getPropertiesWithInfo() {
        List<String> properties = new ArrayList<String>(infoMessages.keySet());
        return properties;
    }

    public void clearErrorMessages() {
        errorMessages.clear();
    }

    public boolean doesPropertyHaveError(String key) {
        return errorMessages.containsKey(key);
    }

    /**
     * @param pattern comma separated list of keys, optionally ending with * wildcard
     */
    public boolean containsKeyMatchingPattern(String pattern) {
        List<String> simplePatterns = new ArrayList<String>();
        List<String> wildcardPatterns = new ArrayList<String>();
        String[] patterns = pattern.split(",");
        for (int i = 0; i < patterns.length; i++) {
            String s = patterns[i];
            if (s.endsWith("*")) {
                wildcardPatterns.add(s.substring(0, s.length() - 1));
            } else {
                simplePatterns.add(s);
            }
        }
        for (Iterator<String> keys = errorMessages.keySet().iterator(); keys.hasNext(); ) {
            String key = keys.next();
            if (simplePatterns.contains(key)) {
                return true;
            }
            for (Iterator<String> wildcardIterator = wildcardPatterns.iterator(); wildcardIterator.hasNext(); ) {
                String wildcard = wildcardIterator.next();
                if (key.startsWith(wildcard)) {
                    return true;
                }
            }
        }
        return false;
    }

    public Set<Map.Entry<String, AutoPopulatingList<ErrorMessage>>> getAllPropertiesAndErrors() {
        return errorMessages.entrySet();
    }

    public AutoPopulatingList<ErrorMessage> getErrorMessagesForProperty(String propertyName) {
        return errorMessages.get(propertyName);
    }

    public AutoPopulatingList<ErrorMessage> getWarningMessagesForProperty(String propertyName) {
        return warningMessages.get(propertyName);
    }

    public AutoPopulatingList<ErrorMessage> getInfoMessagesForProperty(String propertyName) {
        return infoMessages.get(propertyName);
    }

    /**
     * Gets a list of lists that represent errors that matched by the
     * propertyName passed in (multiple lists because the wildcard can match
     * multiple keys). If wildcard is true, the propertyName ends with a
     * wildcard character. Otherwise, it will only match on the single key and
     * return a list with one list
     *
     * @param propertyName
     * @param allowWildcard
     * @return
     */
    public List<AutoPopulatingList<ErrorMessage>> getErrorMessagesForProperty(
        String propertyName, boolean allowWildcard) {
        List<AutoPopulatingList<ErrorMessage>> foundMessages = new ArrayList<AutoPopulatingList<ErrorMessage>>();
        if (allowWildcard) {
            boolean wildcard = false;
            if (propertyName.endsWith("*")) {
                wildcard = true;
                propertyName = propertyName.substring(0,
                    propertyName.length() - 1);
            }
            for (Iterator<String> keys = errorMessages.keySet().iterator(); keys
                .hasNext(); ) {
                String key = keys.next();
                if (!wildcard && propertyName.equals(key)) {
                    foundMessages.add(errorMessages.get(key));
                    break;
                } else if (wildcard && key.startsWith(propertyName)) {
                    foundMessages.add(errorMessages.get(key));
                }
            }
        } else {
            foundMessages.add(getErrorMessagesForProperty(propertyName));
        }

        return foundMessages;
    }

    /**
     * Gets a list of lists that represent warnings that matched by the
     * propertyName passed in (multiple lists because the wildcard can match
     * multiple keys). If wildcard is true, the propertyName ends with a
     * wildcard character. Otherwise, it will only match on the single key and
     * return a list with one list.
     *
     * @param propertyName
     * @param allowWildcard
     * @return
     */
    public List<AutoPopulatingList<ErrorMessage>> getWarningMessagesForProperty(
        String propertyName, boolean allowWildcard) {
        List<AutoPopulatingList<ErrorMessage>> foundMessages = new ArrayList<AutoPopulatingList<ErrorMessage>>();
        if (allowWildcard) {
            boolean wildcard = false;
            if (propertyName.endsWith("*")) {
                wildcard = true;
                propertyName = propertyName.substring(0,
                    propertyName.length() - 1);
            }
            for (Iterator<String> keys = warningMessages.keySet().iterator(); keys
                .hasNext(); ) {
                String key = keys.next();
                if (!wildcard && propertyName.equals(key)) {
                    foundMessages.add(warningMessages.get(key));
                    break;
                } else if (wildcard && key.startsWith(propertyName)) {
                    foundMessages.add(warningMessages.get(key));
                }
            }
        } else {
            foundMessages.add(getWarningMessagesForProperty(propertyName));
        }

        return foundMessages;
    }

    /**
     * Gets a list of lists that represent info messages that matched by the
     * propertyName passed in (multiple lists because the wildcard can match
     * multiple keys). If wildcard is true, the propertyName ends with a
     * wildcard character. If it is false, it will only match on the single key
     * and return a list with one list.
     *
     * @param propertyName
     * @param allowWildcard
     * @return
     */
    public List<AutoPopulatingList<ErrorMessage>> getInfoMessagesForProperty(
        String propertyName, boolean allowWildcard) {
        List<AutoPopulatingList<ErrorMessage>> foundMessages = new ArrayList<AutoPopulatingList<ErrorMessage>>();
        if (allowWildcard) {
            boolean wildcard = false;
            if (propertyName.endsWith("*")) {
                wildcard = true;
                propertyName = propertyName.substring(0,
                    propertyName.length() - 1);
            }
            for (Iterator<String> keys = infoMessages.keySet().iterator(); keys
                .hasNext(); ) {
                String key = keys.next();
                if (!wildcard && propertyName.equals(key)) {
                    foundMessages.add(infoMessages.get(key));
                    break;
                } else if (wildcard && key.startsWith(propertyName)) {
                    foundMessages.add(infoMessages.get(key));
                }
            }
        } else {
            foundMessages.add(getInfoMessagesForProperty(propertyName));
        }

        return foundMessages;
    }

    public boolean hasErrors() {
        return !errorMessages.isEmpty();
    }

    public boolean hasNoErrors() {
        return errorMessages.isEmpty();
    }

    public boolean hasWarnings() {
        return !warningMessages.isEmpty();
    }

    public boolean hasNoWarnings() {
        return warningMessages.isEmpty();
    }

    public boolean hasInfo() {
        return !infoMessages.isEmpty();
    }

    public boolean hasNoInfo() {
        return infoMessages.isEmpty();
    }

    public boolean hasMessages() {
        if (!errorMessages.isEmpty()
            || !warningMessages.isEmpty()
            || !infoMessages.isEmpty()) {
            return true;
        }
        return false;
    }

    public boolean hasNoMessages() {
        if (errorMessages.isEmpty()
            && warningMessages.isEmpty()
            && infoMessages.isEmpty()) {
            return true;
        }
        return false;
    }

    public Set<String> getAllPropertiesWithErrors() {
        return errorMessages.keySet();
    }

    public Set<String> getAllPropertiesWithWarnings() {
        return warningMessages.keySet();
    }

    public Set<String> getAllPropertiesWithInfo() {
        return infoMessages.keySet();
    }

    public AutoPopulatingList<ErrorMessage> removeAllErrorMessagesForProperty(String property) {
        return errorMessages.remove(property);
    }

    public AutoPopulatingList<ErrorMessage> removeAllWarningMessagesForProperty(String property) {
        return warningMessages.remove(property);
    }

    public AutoPopulatingList<ErrorMessage> removeAllInfoMessagesForProperty(String property) {
        return infoMessages.remove(property);
    }

    public int getNumberOfPropertiesWithErrors() {
        return errorMessages.size();
    }

    public Map<String, AutoPopulatingList<ErrorMessage>> getErrorMessages() {
        return this.errorMessages;
    }

    public Map<String, AutoPopulatingList<ErrorMessage>> getWarningMessages() {
        return this.warningMessages;
    }

    public Map<String, AutoPopulatingList<ErrorMessage>> getInfoMessages() {
        return this.infoMessages;
    }

    @Override
    public boolean equals(Object o) {
        return EqualsBuilder.reflectionEquals(this, o);
    }

    @Override
    public int hashCode() {
        return HashCodeBuilder.reflectionHashCode(this);
    }

    @Override
    public String toString() {
        return ToStringBuilder.reflectionToString(this);
    }
}
