/**
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright (c) 2011-2015 ForgeRock AS. All Rights Reserved
 *
 * The contents of this file are subject to the terms
 * of the Common Development and Distribution License
 * (the License). You may not use this file except in
 * compliance with the License.
 *
 * You can obtain a copy of the License at
 * http://forgerock.org/license/CDDLv1.0.html
 * See the License for the specific language governing
 * permission and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL
 * Header Notice in each file and include the License file
 * at http://forgerock.org/license/CDDLv1.0.html
 * If applicable, add the following below the CDDL Header,
 * with the fields enclosed by brackets [] replaced by
 * your own identifying information:
 * "Portions Copyrighted [year] [name of copyright owner]"
 *
 */
package com.sun.identity.sm;

import com.iplanet.sso.SSOException;
import com.iplanet.sso.SSOToken;
import com.sun.identity.shared.xml.XMLUtils;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.forgerock.openam.upgrade.NewServiceWrapper;
import org.forgerock.openam.upgrade.ServiceSchemaModificationWrapper;
import org.forgerock.openam.upgrade.ServiceSchemaUpgradeWrapper;
import org.forgerock.openam.upgrade.NewSubSchemaWrapper;
import org.forgerock.openam.upgrade.ServerUpgrade;
import org.forgerock.openam.upgrade.SchemaUpgradeWrapper;
import org.forgerock.openam.upgrade.SubSchemaModificationWrapper;
import org.forgerock.openam.upgrade.SubSchemaUpgradeWrapper;
import org.forgerock.openam.upgrade.UpgradeException;
import org.forgerock.openam.upgrade.UpgradeHelper;
import org.forgerock.openam.upgrade.UpgradeUtils;
import org.w3c.dom.Document;
import org.w3c.dom.Node;

/**
 * This class determines how a service schema has changed between the current
 * version and the new version in the war file.
 *
 * @author steve
 */
public class ServiceSchemaModifications {

    private static final List<String> SCHEMA_TYPES = Arrays.asList(new String[]{SMSUtils.GLOBAL_SCHEMA,
        SMSUtils.ORG_SCHEMA, SMSUtils.DYNAMIC_SCHEMA, SMSUtils.USER_SCHEMA, SMSUtils.GROUP_SCHEMA,
        SMSUtils.DOMAIN_SCHEMA, SMSUtils.POLICY_SCHEMA});
    protected String serviceName = null;
    protected Document serviceSchemaDoc = null;
    protected SSOToken adminToken = null;

    private boolean isServiceModified = false;
    private boolean hasSubSchemaChanges = false;
    private boolean hasSchemaChanges = false;
    private Set<SchemaUpgradeWrapper> schemaModifications = null;
    private Map<String, ServiceSchemaUpgradeWrapper> modifications = null;
    private Map<String, SubSchemaUpgradeWrapper> subSchemaChanges = null;
    private NewServiceWrapper newServiceWrapper = null;

    public ServiceSchemaModifications(String serviceName, Document schemaDoc, SSOToken adminToken, boolean newService)
            throws UpgradeException {
        this.serviceName = serviceName;
        this.serviceSchemaDoc = schemaDoc;
        this.adminToken = adminToken;

        if (newService) {
            parseNewServiceDefinition();
        } else {
            parseExistingServiceDefinition();
        }
    }

    public boolean isServiceModified() {
        return isServiceModified;
    }

    public boolean hasSchemaChanges() {
        return hasSchemaChanges;
    }

    public boolean hasSubSchemaChanges() {
        return hasSubSchemaChanges;
    }

    public Set<SchemaUpgradeWrapper> getSchemaModifications() {
        return schemaModifications;
    }

    public Map<String, ServiceSchemaUpgradeWrapper> getServiceModifications() {
        return modifications;
    }

    public Map<String, SubSchemaUpgradeWrapper> getSubSchemaChanges() {
        return subSchemaChanges;
    }

    /**
     * Get the wrapper that wraps the new service and any modifications it might have.
     * @return The service wrapper.
     */
    public NewServiceWrapper getNewServiceWrapper() {
        return newServiceWrapper;
    }

    private void parseNewServiceDefinition() throws UpgradeException {
        //we should only fetch these once to prevent performance problems, also in case of fetchNew to prevent
        //encrypting passwords multiple times.
        final Map<String, ServiceSchemaImpl> newSchemaMap = fetchNewServiceAttributes(serviceSchemaDoc);
        createServiceModifications(newSchemaMap);
        if (UpgradeUtils.debug.messageEnabled()) {
            UpgradeUtils.debug.message("Service " + serviceName + " has been added");
            UpgradeUtils.debug.message((newServiceWrapper.getModifiedSchemaMap().isEmpty() ? "No" : "Some") +
                    " modifications were made to " + serviceName);
        }
    }

    private void parseExistingServiceDefinition() throws UpgradeException {
        //we should only fetch these once to prevent performance problems, also in case of fetchNew to prevent
        //encrypting passwords multiple times.
        Map<String, ServiceSchemaImpl> newSchemaMap = fetchNewServiceAttributes(serviceSchemaDoc);
        Map<String, ServiceSchemaImpl> existingSchemaMap = null;

        try {
            existingSchemaMap = fetchExistingServiceAttributes(serviceName, adminToken);
        } catch (SMSException smse) {
            UpgradeUtils.debug.error("unable to fetch existing service attributes", smse);
            throw new UpgradeException(smse.getMessage());
        } catch (SSOException ssoe) {
            UpgradeUtils.debug.error("unable to fetch existing service attributes", ssoe);
            throw new UpgradeException(ssoe.getMessage());
        }
        if (calculateSchemaChanges(newSchemaMap, existingSchemaMap)) {
            if (UpgradeUtils.debug.messageEnabled()) {
                UpgradeUtils.debug.message("service " + serviceName + " has new/deleted schema");
            }
            hasSchemaChanges = true;
        }

        // sub schemas added or removed?
        if (calculateSubSchemaChanges(newSchemaMap, existingSchemaMap)) {
            if (UpgradeUtils.debug.messageEnabled()) {
                UpgradeUtils.debug.message("service " + serviceName + " has a modified sub schema");
            }

            hasSubSchemaChanges = true;
        }

        // has the service changed
        if (calculateServiceModifications(newSchemaMap, existingSchemaMap)) {
            if (UpgradeUtils.debug.messageEnabled()) {
                UpgradeUtils.debug.message("service " + serviceName + " has changes in schemas");
            }

            isServiceModified = true;
        }
    }

    private static Map<String, ServiceSchemaImpl> getAttributes(Document document) {
        Map<String, ServiceSchemaImpl> schemas = new HashMap<String, ServiceSchemaImpl>();
        Node schemaRoot = XMLUtils.getRootNode(document, SMSUtils.SCHEMA);
        for (final String schemaType : SCHEMA_TYPES) {
            Node childNode = XMLUtils.getChildNode(schemaRoot, schemaType);
            if (childNode != null) {
                schemas.put(schemaType, new ServiceSchemaImpl(childNode));
            }
        }

        return schemas;
    }

    private boolean calculateSchemaChanges(Map<String, ServiceSchemaImpl> newSchemaMap,
            Map<String, ServiceSchemaImpl> existingSchemaMap) throws UpgradeException {
        schemaModifications = new HashSet<SchemaUpgradeWrapper>();
        for (Map.Entry<String, ServiceSchemaImpl> entry : newSchemaMap.entrySet()) {
            String schemaName = entry.getKey();
            ServiceSchemaImpl schema = entry.getValue();
            if (!existingSchemaMap.containsKey(schemaName)) {
                ServiceSchemaModificationWrapper newAttrs = new ServiceSchemaModificationWrapper(serviceName,
                        schemaName, schema.getAttributeSchemas());
                //NB: only schema additions are currently supported.
                schemaModifications.add(new SchemaUpgradeWrapper(newAttrs));
            }
        }

        return !schemaModifications.isEmpty();
    }

    private boolean calculateSubSchemaChanges(Map<String, ServiceSchemaImpl> newSchemaMap,
            Map<String, ServiceSchemaImpl> existingSchemaMap) throws UpgradeException {
        subSchemaChanges = new HashMap<String, SubSchemaUpgradeWrapper>();

        try {
            for (Map.Entry<String, ServiceSchemaImpl> newAttrSchemaEntry : newSchemaMap.entrySet()) {
                SubSchemaModificationWrapper subSchemaAdded =
                        getSubSchemaAdditionsRecursive(newAttrSchemaEntry.getKey(),
                                                       newAttrSchemaEntry.getKey(),
                                                       newAttrSchemaEntry.getValue(),
                                                       existingSchemaMap.get(newAttrSchemaEntry.getKey()));

                if (subSchemaAdded.subSchemaChanged()) {
                    subSchemaChanges.put(newAttrSchemaEntry.getKey(), new SubSchemaUpgradeWrapper(subSchemaAdded));
                }
            }
        } catch (SMSException smse) {
            UpgradeUtils.debug.error("error whilst determining sub schema changes for service: " + serviceName, smse);
            throw new UpgradeException(smse.getMessage());
        }

        if (UpgradeUtils.debug.messageEnabled()) {
            UpgradeUtils.debug.message("calculateSubSchemaChanges returning " + (!(subSchemaChanges.isEmpty())));
        }

        return !(subSchemaChanges.isEmpty());
    }

    private SubSchemaModificationWrapper getSubSchemaAdditionsRecursive(String schemaName,
                                                                        String compoundName,
                                                                        ServiceSchemaImpl newSchema,
                                                                        ServiceSchemaImpl existingSchema)
            throws SMSException {
        SubSchemaModificationWrapper subSchemaAddedResult = new SubSchemaModificationWrapper()  ;

        if (!newSchema.getSubSchemaNames().isEmpty()) {
            for (String subSchemaName : (Set<String>) newSchema.getSubSchemaNames()) {
                if (existingSchema == null || !(existingSchema.getSubSchemaNames().contains(subSchemaName))) {
                    subSchemaAddedResult.put(compoundName + "/" + subSchemaName,
                            new NewSubSchemaWrapper(serviceName,
                                                    subSchemaName,
                                                    newSchema.getSubSchema(subSchemaName).getSchemaNode()));
                } else {
                    SubSchemaModificationWrapper subSchemaResult =
                            getSubSchemaAdditionsRecursive(subSchemaName,
                                                           compoundName + "/" + subSchemaName,
                                                           newSchema.getSubSchema(subSchemaName),
                                                           existingSchema.getSubSchema(subSchemaName));
                    if (subSchemaAddedResult.subSchemaChanged()) {
                        subSchemaAddedResult.setSubSchema(subSchemaResult);
                    }
                }
            }
        }

        return subSchemaAddedResult;
    }

    private boolean calculateServiceModifications(Map<String, ServiceSchemaImpl> newSchemaMap,
            Map<String, ServiceSchemaImpl> existingSchemaMap) throws UpgradeException {
        modifications = new HashMap<String, ServiceSchemaUpgradeWrapper>();

        try {
            for (Map.Entry<String, ServiceSchemaImpl> newAttrSchemaEntry : newSchemaMap.entrySet()) {
                ServiceSchemaImpl schema = existingSchemaMap.get(newAttrSchemaEntry.getKey());
                if (schema == null) {
                    //this is a new schema, that's not available in the existing schema, should be covered by
                    //calculateSchemaChanges
                    continue;
                }
                ServiceSchemaModificationWrapper attrsAdded =
                        getServiceAdditionsRecursive(newAttrSchemaEntry.getKey(),
                                                    newAttrSchemaEntry.getValue(),
                                                    schema);
                ServiceSchemaModificationWrapper attrsModified =
                        getServiceModificationsRecursive(newAttrSchemaEntry.getKey(),
                                                    newAttrSchemaEntry.getValue(),
                                                    schema);
                ServiceSchemaModificationWrapper attrsDeleted =
                        getServiceDeletionsRecursive(newAttrSchemaEntry.getKey(),
                                                    newAttrSchemaEntry.getValue(),
                                                    schema);

                if (attrsAdded.hasBeenModified() || attrsModified.hasBeenModified() || attrsDeleted.hasBeenModified()) {
                    modifications.put(newAttrSchemaEntry.getKey(),
                            new ServiceSchemaUpgradeWrapper(attrsAdded, attrsModified, attrsDeleted));
                }
            }
        } catch (SMSException smse) {
            UpgradeUtils.debug.error("error whilst determining schema changes for service: " + serviceName, smse);
            throw new UpgradeException(smse.getMessage());
        }

        if (UpgradeUtils.debug.messageEnabled()) {
            UpgradeUtils.debug.message("calculateServiceModifications returning " + (!(modifications.isEmpty())));
        }

        return !(modifications.isEmpty());
    }

    private ServiceSchemaModificationWrapper getServiceAdditionsRecursive(String schemaName,
                                                                           ServiceSchemaImpl newSchema,
                                                                           ServiceSchemaImpl existingSchema)
    throws SMSException, UpgradeException {
        Set<AttributeSchemaImpl> attrsAdded = new HashSet<AttributeSchemaImpl>();
        ServiceSchemaModificationWrapper attrAddedResult = new ServiceSchemaModificationWrapper(serviceName, schemaName);

        if (newSchema.getAttributeSchemas() != null) {
            attrsAdded = getAttributesAdded(newSchema.getAttributeSchemas(), existingSchema.getAttributeSchemas());
        }

        if (!(attrsAdded.isEmpty())) {
            attrAddedResult.setAttributes(attrsAdded);
        }

        if (!newSchema.getSubSchemaNames().isEmpty()) {
            for (String subSchemaName : (Set<String>) newSchema.getSubSchemaNames()) {
                if (!(existingSchema.getSubSchemaNames().contains(subSchemaName))) {
                    // new sub schema so skip attribute checking
                    continue;
                }

                ServiceSchemaModificationWrapper subSchemaResult = getServiceAdditionsRecursive(subSchemaName,
                                                    newSchema.getSubSchema(subSchemaName),
                                                    existingSchema.getSubSchema(subSchemaName));

                if (subSchemaResult.hasBeenModified()) {
                    attrAddedResult.addSubSchema(subSchemaName, subSchemaResult);
                }
            }
        }

        return attrAddedResult;
    }

    private ServiceSchemaModificationWrapper getServiceModificationsRecursive(String schemaName,
                                                                           ServiceSchemaImpl newSchema,
                                                                           ServiceSchemaImpl existingSchema)
    throws SMSException, UpgradeException {
        Set<AttributeSchemaImpl> attrsModified = new HashSet<AttributeSchemaImpl>();
        ServiceSchemaModificationWrapper attrModifiedResult = new ServiceSchemaModificationWrapper(serviceName, schemaName);

        if (newSchema.getAttributeSchemas() != null) {
            attrsModified = getAttributesModified(newSchema.getAttributeSchemas(), existingSchema.getAttributeSchemas());
        }

        if (!(attrsModified.isEmpty())) {
            attrModifiedResult.setAttributes(attrsModified);
        }

        if (!newSchema.getSubSchemaNames().isEmpty()) {
            for (String subSchemaName : (Set<String>) newSchema.getSubSchemaNames()) {
                if (!(existingSchema.getSubSchemaNames().contains(subSchemaName))) {
                    // new sub schema so skip attribute checking
                    continue;
                }

                ServiceSchemaModificationWrapper subSchemaResult = getServiceModificationsRecursive(subSchemaName,
                                                    newSchema.getSubSchema(subSchemaName),
                                                    existingSchema.getSubSchema(subSchemaName));

                if (subSchemaResult.hasBeenModified()) {
                    attrModifiedResult.addSubSchema(subSchemaName, subSchemaResult);
                }
            }
        }

        return attrModifiedResult;
    }

    private ServiceSchemaModificationWrapper getServiceDeletionsRecursive(String schemaName,
                                                                           ServiceSchemaImpl newSchema,
                                                                           ServiceSchemaImpl existingSchema)
    throws SMSException {
        Set<AttributeSchemaImpl> attrsDeleted = new HashSet<AttributeSchemaImpl>();
        ServiceSchemaModificationWrapper attrDeletedResult = new ServiceSchemaModificationWrapper(serviceName, schemaName);;

        if (newSchema.getAttributeSchemas() != null) {
            attrsDeleted = getAttributesDeleted(newSchema.getAttributeSchemas(), existingSchema.getAttributeSchemas());
        }

        if (!(attrsDeleted.isEmpty())) {
            attrDeletedResult.setAttributes(attrsDeleted);
        }

        if (!newSchema.getSubSchemaNames().isEmpty()) {
            for (String subSchemaName : (Set<String>) newSchema.getSubSchemaNames()) {
                if (!(existingSchema.getSubSchemaNames().contains(subSchemaName))) {
                    // new sub schema so skip attribute checking
                    continue;
                }

                ServiceSchemaModificationWrapper subSchemaResult = getServiceDeletionsRecursive(subSchemaName,
                                                    newSchema.getSubSchema(subSchemaName),
                                                    existingSchema.getSubSchema(subSchemaName));

                if (subSchemaResult.hasBeenModified()) {
                    attrDeletedResult.addSubSchema(subSchemaName, subSchemaResult);
                }
            }
        }

        return attrDeletedResult;
    }

    private Set<AttributeSchemaImpl> getAttributesAdded(Set<AttributeSchemaImpl> newAttrs,
            Set<AttributeSchemaImpl> existingAttrs) throws UpgradeException {
        Set<AttributeSchemaImpl> attrAdded = new HashSet<AttributeSchemaImpl>();

        for (AttributeSchemaImpl newAttr : newAttrs) {
            boolean found = false;
            for (AttributeSchemaImpl existingAttr : existingAttrs) {
                if (newAttr.getName().equals(existingAttr.getName())) {
                    found = true;
                }
            }

            if (!found) {
                UpgradeHelper serviceHelper = ServerUpgrade.getServiceHelper(serviceName);
                if (serviceHelper != null) {
                    newAttr = serviceHelper.addNewAttribute(existingAttrs, newAttr);
                }

                attrAdded.add(newAttr);
            }
        }

        return attrAdded;
    }

    private Set<AttributeSchemaImpl> getAttributesModified(Set<AttributeSchemaImpl> newAttrs, Set<AttributeSchemaImpl> existingAttrs)
    throws UpgradeException {
        Set<AttributeSchemaImpl> attrMods = new HashSet<AttributeSchemaImpl>();

        for (AttributeSchemaImpl newAttr : newAttrs) {
            // skip attributes that are not explicitly named for upgrade
            if (ServerUpgrade.getServiceHelper(serviceName) == null ||
                    !ServerUpgrade.getServiceHelper(serviceName).getAttributes().contains(newAttr.getName())) {
                continue;
            }

            for (AttributeSchemaImpl existingAttr : existingAttrs) {
                if (!existingAttr.getName().equals(newAttr.getName())) {
                    continue;
                }

                try {
                    UpgradeHelper helper = ServerUpgrade.getServiceHelper(serviceName);
                    AttributeSchemaImpl upgradedAttr = helper.upgradeAttribute(existingAttr, newAttr);

                    if (upgradedAttr != null) {
                        attrMods.add(upgradedAttr);
                    }
                } catch (UpgradeException ue) {
                    UpgradeUtils.debug.error("Unable to process upgrade helper", ue);
                    throw ue;
                }
            }
        }

        return attrMods;
    }

    private Set<AttributeSchemaImpl> getAttributesDeleted(Set<AttributeSchemaImpl> newAttrs, Set<AttributeSchemaImpl> existingAttrs) {
        Set<AttributeSchemaImpl> attrDeleted = new HashSet<AttributeSchemaImpl>();

        for (AttributeSchemaImpl existingAttr : existingAttrs) {
            boolean found = false;
            for (AttributeSchemaImpl newAttr  : newAttrs) {
                if (existingAttr.getName().equals(newAttr.getName())) {
                    found = true;
                }
            }

            if (!found) {
                attrDeleted.add(existingAttr);
            }
        }

        return attrDeleted;
    }

    private Map<String, ServiceSchemaImpl> fetchNewServiceAttributes(Document doc)
    throws UpgradeException {
        try {
            ServiceManager.checkAndEncryptPasswordSyntax(doc, true);
        } catch (SMSException smse) {
            UpgradeUtils.debug.error("Unable to encrypt default values for passwords");
            throw new UpgradeException(smse);
        }
        Map<String, ServiceSchemaImpl> schemas = getAttributes(doc);

        return schemas;
    }

    private Map<String, ServiceSchemaImpl> fetchExistingServiceAttributes(String serviceName, SSOToken adminToken)
    throws SMSException, SSOException {
        ServiceSchemaManager ssm = new ServiceSchemaManager(serviceName, adminToken);
        Map<String, ServiceSchemaImpl> schemas = getAttributes(ssm.getDocumentCopy());

        return schemas;
    }

    /**
     * This will use the service schemas to find any upgrade handlers registered for the service and
     * populate the NewServiceWrapper's ServiceSchemaModificationWrappers.
     * @param newSchemaMap The service schemas representing the service.
     * @throws UpgradeException If an upgrade error occurs.
     */
    private void createServiceModifications(Map<String, ServiceSchemaImpl> newSchemaMap) throws UpgradeException {
        try {
            final Map<String, ServiceSchemaModificationWrapper> serviceSchemaMap =
                    new HashMap<String, ServiceSchemaModificationWrapper>();
            for (Map.Entry<String, ServiceSchemaImpl> newAttrSchemaEntry : newSchemaMap.entrySet()) {
                final ServiceSchemaModificationWrapper attributesAdded =
                        getServiceModificationsRecursive(newAttrSchemaEntry.getKey(), newAttrSchemaEntry.getValue());

                if (attributesAdded.hasBeenModified()) {
                    serviceSchemaMap.put(newAttrSchemaEntry.getKey(), attributesAdded);
                }
            }
            newServiceWrapper = new NewServiceWrapper(serviceName, serviceSchemaMap, serviceSchemaDoc);
        } catch (SMSException smse) {
            UpgradeUtils.debug.error("Error whilst determining schema changes for service: " + serviceName, smse);
            throw new UpgradeException(smse.getMessage(), smse);
        }
    }

    /**
     * This will recursively go through the service schema and add modifications if any was found.
     *
     * @param schemaName Name of the schema being processed.
     * @param newSchema The schema being processed.
     * @return The schema modification wrapper.
     * @throws UpgradeException If an error occurred during attribute upgrade.
     */
    private ServiceSchemaModificationWrapper getServiceModificationsRecursive(String schemaName,
                                                                              ServiceSchemaImpl newSchema)
            throws SMSException, UpgradeException {

        final ServiceSchemaModificationWrapper attrModifiedResult =
                new ServiceSchemaModificationWrapper(serviceName, schemaName);

        if (newSchema.getAttributeSchemas() != null) {
            final Set<AttributeSchemaImpl> attrsModified = getAttributesModified(newSchema.getAttributeSchemas());
            if (!attrsModified.isEmpty()) {
                attrModifiedResult.setAttributes(attrsModified);
            }
        }

        if (!newSchema.getSubSchemaNames().isEmpty()) {
            for (String subSchemaName : (Set<String>) newSchema.getSubSchemaNames()) {
                final ServiceSchemaModificationWrapper subSchemaResult =
                        getServiceModificationsRecursive(subSchemaName, newSchema.getSubSchema(subSchemaName));
                if (subSchemaResult.hasBeenModified()) {
                    attrModifiedResult.addSubSchema(subSchemaName, subSchemaResult);
                }
            }
        }

        return attrModifiedResult;
    }

    /**
     * This will find the service helper specified for a newly added service if one is registered.
     * If any attributes are registered they will be passed to the helper for modification.
     * @param newAttrs The new attributes being added by this service.
     * @return The modified attributes.
     * @throws UpgradeException If an error occurred during attribute upgrade.
     */
    private Set<AttributeSchemaImpl> getAttributesModified(Set<AttributeSchemaImpl> newAttrs) throws UpgradeException {
        final Set<AttributeSchemaImpl> attrMods = new HashSet<AttributeSchemaImpl>();
        final UpgradeHelper helper = ServerUpgrade.getServiceHelper(serviceName);

        if (helper != null) {
            for (AttributeSchemaImpl newAttr : newAttrs) {
                // skip attributes that are not explicitly named for upgrade
                if (!helper.getAttributes().contains(newAttr.getName())) {
                    continue;
                }
                try {
                    final AttributeSchemaImpl upgradedAttr = helper.upgradeAttribute(newAttr);
                    if (upgradedAttr != null) {
                        attrMods.add(upgradedAttr);
                    }
                } catch (UpgradeException ue) {
                    UpgradeUtils.debug.error("Unable to process upgrade helper for service: " + serviceName, ue);
                    throw ue;
                }
            }
        }

        return attrMods;
    }
}
