Skip to content
Snippets Groups Projects
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
NamesService.java 46.72 KiB
/*
 * Copyright (C) 2021 European Spallation Source ERIC.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 */

package org.openepics.names.service;

import java.text.MessageFormat;
import java.util.Date;
import java.util.List;
import java.util.UUID;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.apache.commons.lang3.StringUtils;
import org.openepics.names.repository.DeviceGroupRepository;
import org.openepics.names.repository.DeviceTypeRepository;
import org.openepics.names.repository.DisciplineRepository;
import org.openepics.names.repository.IDeviceGroupRepository;
import org.openepics.names.repository.IDeviceTypeRepository;
import org.openepics.names.repository.IDisciplineRepository;
import org.openepics.names.repository.INameRepository;
import org.openepics.names.repository.ISubsystemRepository;
import org.openepics.names.repository.ISystemGroupRepository;
import org.openepics.names.repository.ISystemRepository;
import org.openepics.names.repository.NameRepository;
import org.openepics.names.repository.SubsystemRepository;
import org.openepics.names.repository.SystemGroupRepository;
import org.openepics.names.repository.SystemRepository;
import org.openepics.names.repository.model.DeviceGroup;
import org.openepics.names.repository.model.DeviceType;
import org.openepics.names.repository.model.Discipline;
import org.openepics.names.repository.model.Name;
import org.openepics.names.repository.model.Structure;
import org.openepics.names.repository.model.Subsystem;
import org.openepics.names.repository.model.System;
import org.openepics.names.repository.model.SystemGroup;
import org.openepics.names.rest.beans.FieldName;
import org.openepics.names.rest.beans.Status;
import org.openepics.names.rest.beans.element.NameElement;
import org.openepics.names.rest.beans.element.NameElementCommand;
import org.openepics.names.rest.beans.response.ResponsePageNameElements;
import org.openepics.names.util.EssNamingConvention;
import org.openepics.names.util.ExceptionUtil;
import org.openepics.names.util.HolderIRepositories;
import org.openepics.names.util.HolderRepositories;
import org.openepics.names.util.HolderStructures;
import org.openepics.names.util.NameElementUtil;
import org.openepics.names.util.NameUtil;
import org.openepics.names.util.TextUtil;
import org.openepics.names.util.ValidateNameElementUtil;
import org.openepics.names.util.ValidateUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import com.google.common.collect.Lists;

/**
 * This class provides names services.
 *
 * @author Lars Johansson
 */
@Service
public class NamesService {

    // note
    //     handling of system structure, device structure
    //         parent system structure uuid (system group, system, subsystem) - ability to find structure
    //         parent device structure uuid (device type)
    //     holder
    //         HolderIRepositories and HolderSystemDeviceStructure may or may not be used for preparation of what to return
    //     latest
    //         automatically not show names that do not come into play
    //         = automatically exclude (approved and not latest)
    //         otherwise refer to history
    //     namecommand
    //         cud - create update delete

    private static final Logger LOGGER = Logger.getLogger(NamesService.class.getName());

    private static final String UPDATE_AFTER_APPROVE_STRUCTURE_CHANGE = "Update after APPROVE structure change";

    private HolderIRepositories holderIRepositories;
    private HolderRepositories holderRepositories;
    // convenience, also part of holderRepositories
    private NameRepository nameRepository;
    private EssNamingConvention namingConvention;

    @Autowired
    public NamesService(
            INameRepository iNameRepository,
            ISystemGroupRepository iSystemGroupRepository,
            ISystemRepository iSystemRepository,
            ISubsystemRepository iSubsystemRepository,
            IDisciplineRepository iDisciplineRepository,
            IDeviceGroupRepository iDeviceGroupRepository,
            IDeviceTypeRepository iDeviceTypeRepository,
            NameRepository nameRepository,
            SystemGroupRepository systemGroupRepository,
            SystemRepository systemRepository,
            SubsystemRepository subsystemRepository,
            DisciplineRepository disciplineRepository,
            DeviceGroupRepository deviceGroupRepository,
            DeviceTypeRepository deviceTypeRepository) {

        this.namingConvention = new EssNamingConvention();
        this.holderIRepositories = new HolderIRepositories(
                iNameRepository,
                iSystemGroupRepository,
                iSystemRepository,
                iSubsystemRepository,
                iDisciplineRepository,
                iDeviceGroupRepository,
                iDeviceTypeRepository);
        this.holderRepositories = new HolderRepositories(
                nameRepository,
                systemGroupRepository,
                systemRepository,
                subsystemRepository,
                disciplineRepository,
                deviceGroupRepository,
                deviceTypeRepository);
        this.nameRepository = nameRepository;
    }
    @Transactional
    public List<NameElement> createNames(List<NameElementCommand> nameElementCommands, String username) {
        // validation outside method
        // transaction
        //     for each name element
        //         create name, latest, with data
        //         handle name element for created name
        //     no notify
        //     return name elements for created names

        // initiate holder of system and device structure content, for performance reasons
        HolderStructures holderStructures = new HolderStructures(holderIRepositories);

        Date when = new Date();
        final List<NameElement> createdNameElements = Lists.newArrayList();
        for (NameElementCommand nameElementCommand : nameElementCommands) {
            NameElement createdNameElement = createName(nameElementCommand, when, username, holderStructures);
            createdNameElements.add(createdNameElement);

            if (LOGGER.isLoggable(Level.FINER)) {
                LOGGER.log(Level.FINER, MessageFormat.format(TextUtil.DESCRIPTION_NAME_VALUE, TextUtil.CREATE_NAME, TextUtil.ELEMENT_IN, nameElementCommand));
                LOGGER.log(Level.FINER, MessageFormat.format(TextUtil.DESCRIPTION_NAME_VALUE, TextUtil.CREATE_NAME, TextUtil.ELEMENT_OUT, createdNameElement));
            }
        }

        LOGGER.log(Level.INFO,
                () -> MessageFormat.format(
                        TextUtil.DESCRIPTION_NUMBER_ELEMENTS_IN_OUT,
                        "Create names",
                        nameElementCommands.size(),
                        createdNameElements.size()));
        return createdNameElements;
    }
    @Transactional(propagation = Propagation.MANDATORY)
    public NameElement createName(NameElementCommand nameElementCommand, Date when, String username, HolderStructures holderStructures) {
        // validation outside method
        // transaction
        //     support a current transaction, throw an exception if none exists
        //     attributes
        //     find
        //     prepare
        //     create - approved, latest, not deleted, uuid
        //     return name element for created name
        //
        // attributes
        //     parentSystemStructure, parentDeviceStructure, index, description, comment

        UUID parentSystemStructure = nameElementCommand.getParentSystemStructure();
        UUID parentDeviceStructure = nameElementCommand.getParentDeviceStructure();
        String index = nameElementCommand.getIndex();
        String description = nameElementCommand.getDescription();
        String comment = nameElementCommand.getComment();

        // find
        //     system structure - system group, system, subsystem - one of the three expected to be non-null, other two expected to be null
        //     device structure - device type - may be null
        SystemGroup systemGroup = holderIRepositories.systemGroupRepository().findLatestByUuid(parentSystemStructure.toString());
        System      system      = holderIRepositories.systemRepository().findLatestByUuid(parentSystemStructure.toString());
        Subsystem   subsystem   = holderIRepositories.subsystemRepository().findLatestByUuid(parentSystemStructure.toString());
        DeviceType  deviceType  = null;
        if (parentDeviceStructure != null) {
            deviceType = holderIRepositories.deviceTypeRepository().findLatestByUuid(parentDeviceStructure.toString());
        }

        String derivedName = null;
        if (systemGroup != null) {
            derivedName = NameUtil.getName(systemGroup, deviceType, index, holderStructures);
        } else if (system != null) {
            derivedName = NameUtil.getName(system, deviceType, index, holderStructures);
        } else if (subsystem != null) {
            derivedName = NameUtil.getName(subsystem, deviceType, index, holderStructures);
        }

        // create
        Name name = new Name(UUID.randomUUID(),
                systemGroup != null ? systemGroup.getUuid() : null,
                system != null ? system.getUuid() : null,
                subsystem != null ? subsystem.getUuid() : null,
                parentDeviceStructure,
                index, derivedName, namingConvention.equivalenceClassRepresentative(derivedName),
                description, Status.APPROVED, Boolean.TRUE, Boolean.FALSE,
                when, username, comment);
        nameRepository.createName(name);

        return NameElementUtil.getNameElement(name);
    }

    // ----------------------------------------------------------------------------------------------------

    public ResponsePageNameElements readNames(Boolean deleted,
            String uuid, String name, String systemStructure, String deviceStructure, String index, String description, String who,
            FieldName orderBy, Boolean isAsc, Integer offset, Integer limit) {
        return readNames(deleted,
                uuid, name, systemStructure, deviceStructure, index, description, who,
                Boolean.FALSE, orderBy, isAsc, offset, limit);
    }

    public ResponsePageNameElements readNames(Boolean deleted,
            String uuid, String name, String systemStructure, String deviceStructure, String index, String description, String who,
            Boolean includeHistory, FieldName orderBy, Boolean isAsc, Integer offset, Integer limit) {
        // validation outside method
        // read names
        // return name elements for names

        if (LOGGER.isLoggable(Level.FINE)) {
            LOGGER.log(Level.FINE, MessageFormat.format(TextUtil.DESCRIPTION_NAME_VALUE, TextUtil.READ_NAMES, "deleted", deleted));
            LOGGER.log(Level.FINE, MessageFormat.format(TextUtil.DESCRIPTION_NAME_VALUE, TextUtil.READ_NAMES, "uuid", uuid));
            LOGGER.log(Level.FINE, MessageFormat.format(TextUtil.DESCRIPTION_NAME_VALUE, TextUtil.READ_NAMES, "name", name));
            LOGGER.log(Level.FINE, MessageFormat.format(TextUtil.DESCRIPTION_NAME_VALUE, TextUtil.READ_NAMES, "systemStructure", systemStructure));
            LOGGER.log(Level.FINE, MessageFormat.format(TextUtil.DESCRIPTION_NAME_VALUE, TextUtil.READ_NAMES, "deviceStructure", deviceStructure));
            LOGGER.log(Level.FINE, MessageFormat.format(TextUtil.DESCRIPTION_NAME_VALUE, TextUtil.READ_NAMES, "index", index));
            LOGGER.log(Level.FINE, MessageFormat.format(TextUtil.DESCRIPTION_NAME_VALUE, TextUtil.READ_NAMES, "description", description));
            LOGGER.log(Level.FINE, MessageFormat.format(TextUtil.DESCRIPTION_NAME_VALUE, TextUtil.READ_NAMES, "who", who));
            LOGGER.log(Level.FINE, MessageFormat.format(TextUtil.DESCRIPTION_NAME_VALUE, TextUtil.READ_NAMES, "includeHistory", includeHistory));
            LOGGER.log(Level.FINE, MessageFormat.format(TextUtil.DESCRIPTION_NAME_VALUE, TextUtil.READ_NAMES, "orderBy", orderBy));
            LOGGER.log(Level.FINE, MessageFormat.format(TextUtil.DESCRIPTION_NAME_VALUE, TextUtil.READ_NAMES, "isAsc", isAsc));
            LOGGER.log(Level.FINE, MessageFormat.format(TextUtil.DESCRIPTION_NAME_VALUE, TextUtil.READ_NAMES, "offset", offset));
            LOGGER.log(Level.FINE, MessageFormat.format(TextUtil.DESCRIPTION_NAME_VALUE, TextUtil.READ_NAMES, "limit", limit));
        }

        List<Name> names = null;
        Long totalCount = null;
        if (Boolean.TRUE.equals(includeHistory)) {
            names = nameRepository.readNamesHistory(uuid, name, null, systemStructure, deviceStructure, index, description, who, orderBy, isAsc, offset, limit);
            totalCount = nameRepository.countNamesHistory(uuid, name, null, systemStructure, deviceStructure, index, description, who);
        } else {
            names = nameRepository.readNames(deleted, uuid, name, null, systemStructure, deviceStructure, index, description, who, includeHistory, orderBy, isAsc, offset, limit);
            totalCount = nameRepository.countNames(deleted, uuid, name, null, systemStructure, deviceStructure, index, description, who, includeHistory);
        }

        // one name entry will give one name element
        //     history does not affect totalCount, sorting, pagination

        final List<NameElement> nameElements = NameElementUtil.getNameElements(names);

        ResponsePageNameElements response = new ResponsePageNameElements(nameElements, totalCount, nameElements.size(), offset, limit);
        LOGGER.log(Level.FINE,
                () -> MessageFormat.format(
                        TextUtil.DESCRIPTION_RESPONSE,
                        TextUtil.READ_NAMES,
                        response.toString()));
        return response;
    }

    public ResponsePageNameElements readNames(String uuid) {
        // validation outside method
        // read uuid or name
        // return name elements
        //     caller to handle name element(s)

        LOGGER.log(Level.FINE, () -> MessageFormat.format(TextUtil.DESCRIPTION_NAME_VALUE, TextUtil.READ_NAMES, "uuid", uuid));

        final List<NameElement> nameElements = Lists.newArrayList();
        try {
            UUID.fromString(uuid);
            Name latestByUuid = holderIRepositories.nameRepository().findLatestByUuid(uuid);
            if (latestByUuid != null) {
                nameElements.add(NameElementUtil.getNameElement(latestByUuid));
                return new ResponsePageNameElements(nameElements, Long.valueOf(nameElements.size()), nameElements.size(), -1, -1);
            }
        } catch (IllegalArgumentException e) {
            return readNames(false,
                    null, uuid, null, null, null, null, null,
                    null, null, null, null);
        }

        return new ResponsePageNameElements(nameElements, Long.valueOf(nameElements.size()), nameElements.size(), -1, -1);
    }

    public ResponsePageNameElements readNamesSystemStructure(String mnemonicPath, Boolean deleted,
            FieldName orderBy, Boolean isAsc, Integer offset, Integer limit) {
        // validation outside method
        // read mnemonic path
        // return name elements

        LOGGER.log(Level.FINE, () -> MessageFormat.format(TextUtil.DESCRIPTION_NAME_VALUE, TextUtil.READ_NAMES_SYSTEM_STRUCTURE, "mnemonicPath", mnemonicPath));

        return readNames(deleted,
                null, null, mnemonicPath, null, null, null, null,
                orderBy, isAsc, offset, limit);
    }

    public ResponsePageNameElements readNamesDeviceStructure(String mnemonicPath, Boolean deleted,
            FieldName orderBy, Boolean isAsc, Integer offset, Integer limit) {
        // validation outside method
        // read mnemonic path
        // return name elements

        LOGGER.log(Level.FINE, () -> MessageFormat.format(TextUtil.DESCRIPTION_NAME_VALUE, TextUtil.READ_NAMES_DEVICE_STRUCTURE, "mnemonicPath", mnemonicPath));

        return readNames(deleted,
                null, null, null,mnemonicPath, null, null, null,
                orderBy, isAsc, offset, limit);
    }

    public ResponsePageNameElements readNamesHistory(
            String uuid, String name, String systemStructure, String deviceStructure, String index, String description, String who,
            FieldName orderBy, Boolean isAsc, Integer offset, Integer limit) {
        // validation outside method
        // read history for name
        // return name elements for names

        if (LOGGER.isLoggable(Level.FINE)) {
            LOGGER.log(Level.FINE, MessageFormat.format(TextUtil.DESCRIPTION_NAME_VALUE, TextUtil.READ_NAMES_HISTORY, "uuid", uuid));
            LOGGER.log(Level.FINE, MessageFormat.format(TextUtil.DESCRIPTION_NAME_VALUE, TextUtil.READ_NAMES_HISTORY, "name", name));
            LOGGER.log(Level.FINE, MessageFormat.format(TextUtil.DESCRIPTION_NAME_VALUE, TextUtil.READ_NAMES_HISTORY, "systemStructure", systemStructure));
            LOGGER.log(Level.FINE, MessageFormat.format(TextUtil.DESCRIPTION_NAME_VALUE, TextUtil.READ_NAMES_HISTORY, "deviceStructure", deviceStructure));
            LOGGER.log(Level.FINE, MessageFormat.format(TextUtil.DESCRIPTION_NAME_VALUE, TextUtil.READ_NAMES_HISTORY, "index", index));
            LOGGER.log(Level.FINE, MessageFormat.format(TextUtil.DESCRIPTION_NAME_VALUE, TextUtil.READ_NAMES_HISTORY, "description", description));
            LOGGER.log(Level.FINE, MessageFormat.format(TextUtil.DESCRIPTION_NAME_VALUE, TextUtil.READ_NAMES_HISTORY, "who", who));
            LOGGER.log(Level.FINE, MessageFormat.format(TextUtil.DESCRIPTION_NAME_VALUE, TextUtil.READ_NAMES_HISTORY, "orderBy", orderBy));
            LOGGER.log(Level.FINE, MessageFormat.format(TextUtil.DESCRIPTION_NAME_VALUE, TextUtil.READ_NAMES_HISTORY, "isAsc", isAsc));
            LOGGER.log(Level.FINE, MessageFormat.format(TextUtil.DESCRIPTION_NAME_VALUE, TextUtil.READ_NAMES_HISTORY, "offset", offset));
            LOGGER.log(Level.FINE, MessageFormat.format(TextUtil.DESCRIPTION_NAME_VALUE, TextUtil.READ_NAMES_HISTORY, "limit", limit));
        }

        return readNames(null,
                uuid, name, systemStructure, deviceStructure, index, description, who,
                Boolean.TRUE, orderBy, isAsc, offset, limit);
    }

    public ResponsePageNameElements readNamesHistory(String uuid,
            FieldName orderBy, Boolean isAsc, Integer offset, Integer limit) {
        // validation outside method
        // read history for name
        // return name elements for names

        LOGGER.log(Level.FINE, () -> MessageFormat.format(TextUtil.DESCRIPTION_NAME_VALUE, TextUtil.READ_NAMES_HISTORY, "uuid", uuid));

        return readNames(null,
                uuid, null, null, null, null, null, null,
                Boolean.TRUE, orderBy, isAsc, offset, limit);
    }

    public ResponsePageNameElements readNamesLegacy(String name,
            FieldName orderBy, Boolean isAsc, Integer offset, Integer limit) {
        // validation outside method
        // read legacy names
        // return name elements for names

        if (LOGGER.isLoggable(Level.FINE)) {
            LOGGER.log(Level.FINE, MessageFormat.format(TextUtil.DESCRIPTION_NAME_VALUE, TextUtil.READ_NAMES, "name", name));
            LOGGER.log(Level.FINE, MessageFormat.format(TextUtil.DESCRIPTION_NAME_VALUE, TextUtil.READ_NAMES, "orderBy", orderBy));
            LOGGER.log(Level.FINE, MessageFormat.format(TextUtil.DESCRIPTION_NAME_VALUE, TextUtil.READ_NAMES, "isAsc", isAsc));
            LOGGER.log(Level.FINE, MessageFormat.format(TextUtil.DESCRIPTION_NAME_VALUE, TextUtil.READ_NAMES, "offset", offset));
            LOGGER.log(Level.FINE, MessageFormat.format(TextUtil.DESCRIPTION_NAME_VALUE, TextUtil.READ_NAMES, "limit", limit));
        }

        List<Name> names = nameRepository.readNamesLegacy(name, orderBy, isAsc, offset, limit);
        Long totalCount = nameRepository.countNamesLegacy(name);

        final List<NameElement> nameElements = NameElementUtil.getNameElements(names);

        ResponsePageNameElements response = new ResponsePageNameElements(nameElements, totalCount, nameElements.size(), offset, limit);
        LOGGER.log(Level.FINE,
                () -> MessageFormat.format(
                        TextUtil.DESCRIPTION_RESPONSE,
                        TextUtil.READ_NAMES,
                        response.toString()));
        return response;
    }

    public ResponsePageNameElements readNamesStructure(String uuid, Boolean includeIndirect,
            FieldName orderBy, Boolean isAsc, Integer offset, Integer limit) {
        // validation outside method
        // read by structure uuid
        // return name elements

        LOGGER.log(Level.FINE, () -> MessageFormat.format(TextUtil.DESCRIPTION_NAME_VALUE, TextUtil.READ_NAMES_STRUCTURE, "uuid", uuid));
        LOGGER.log(Level.FINE, () -> MessageFormat.format(TextUtil.DESCRIPTION_NAME_VALUE, TextUtil.READ_NAMES_STRUCTURE, "includeIndirect", includeIndirect));

        // note
        //     approved, latest
        //     may need to have deleted parameter
        //         may solve with queries like
        //             and n.deleted in (false, true)

        // there are different kinds of references to structure ids for names
        //     direct and indirect
        //     use INameRepository
        //     directly
        //         findLatestBySystemGroupUuid(String)
        //         findLatestBySystemUuid(String)
        //         findLatestBySubsystemUuid(String)
        //         findLatestByDeviceTypeUuid(String)
        //     indirectly
        //         findLatestBySystemGroupUuidThroughSystem(String)
        //         findLatestBySystemGroupUuidThroughSubsystem(String)
        //         findLatestBySystemUuidThroughSubsystem(String)
        //         findLatestByDisciplineUuidThroughDeviceType(String)
        //         findLatestByDeviceGroupUuidThroughDeviceType(String)
        //     not have - no name refers directly to discipline uuid, device group uuid
        //         findLatestByDisciplineUuid(String)
        //         findLatestByDisciplineUuidThroughDeviceGroup(String)
        //         findLatestByDeviceGroupUuid(String)

        // go through all structures and see if / where uuid is found
        //     system structure and device structure
        //     uuid in 0 or 1 of kind of structure
        //     negligible impact on performance for going through different structures

        // pagination
        //     separate pagination when result may be from multiple read operations
        //     read operations should not contain pagination in such cases

        // IS THERE NEED TO MAKE SURE NAMES NOT FOUND AND ADDED TWICE ( OR MORE ) TO RESULT ?
        //     is java.util.Set is used, then ordering and pagination will be messy
        //     for start, not consider this

        // read - directly
        List<Name> names = holderIRepositories.nameRepository().findLatestBySystemGroupUuid(uuid);
        names.addAll(holderIRepositories.nameRepository().findLatestBySystemUuid(uuid));
        names.addAll(holderIRepositories.nameRepository().findLatestBySubsystemUuid(uuid));
        names.addAll(holderIRepositories.nameRepository().findLatestByDeviceTypeUuid(uuid));

        if (Boolean.TRUE.equals(includeIndirect)) {
            // indirectly - additional read
            names.addAll(holderIRepositories.nameRepository().findLatestBySystemGroupUuidThroughSystem(uuid));
            names.addAll(holderIRepositories.nameRepository().findLatestBySystemGroupUuidThroughSubsystem(uuid));
            names.addAll(holderIRepositories.nameRepository().findLatestBySystemUuidThroughSubsystem(uuid));
            names.addAll(holderIRepositories.nameRepository().findLatestByDisciplineUuidThroughDeviceType(uuid));
            names.addAll(holderIRepositories.nameRepository().findLatestByDeviceGroupUuidThroughDeviceType(uuid));
        }

        // pagination
        Long totalCount = Long.valueOf(names.size());
        final List<NameElement> nameElements = paginate(NameElementUtil.getNameElements(names), offset, limit);
        ResponsePageNameElements response = new ResponsePageNameElements(nameElements, totalCount, nameElements.size(), offset, limit);

        LOGGER.log(Level.FINE,
                () -> MessageFormat.format(
                        TextUtil.DESCRIPTION_RESPONSE,
                        TextUtil.READ_NAMES_STRUCTURE,
                        response.toString()));
        return response;
    }

    private List<NameElement> paginate(List<NameElement> list, Integer offset, Integer limit) {
        // intended as pagination when list may contain result from multiple read operations
        //     read operations should not contain pagination in such cases
        if (offset == null || limit == null) {
            return list;
        }

        List<NameElement> listPagination = Lists.newArrayList();
        int offsetItems = offset * limit;
        int availableItems = list.size() - offsetItems;
        if (availableItems > 0) {
            for (int i = offsetItems; i < Math.min(offsetItems+availableItems, offsetItems+limit); i++) {
                listPagination.add(list.get(i));
            }
        }
        return listPagination;
    }

    // ----------------------------------------------------------------------------------------------------

    public Boolean existsName(String name) {
        // validation outside method
        // read exists

        LOGGER.log(Level.FINE, () -> MessageFormat.format(TextUtil.DESCRIPTION_NAME_VALUE, TextUtil.EXISTS_NAME, "name", name));

        List<Name> names = nameRepository.readNames(false,
                null, name, null, null, null, null, null, null);
        return !names.isEmpty();
    }

    public Boolean isLegacyName(String name) {
        // validation outside method
        // read names
        // note
        //     legacy - name exists but either parent does not exist (latest, not deleted, by uuid)

        LOGGER.log(Level.FINE, () -> MessageFormat.format(TextUtil.DESCRIPTION_NAME_VALUE, TextUtil.IS_LEGACY_NAME, "name", name));

        List<Name> names = nameRepository.readNames(false, null, name, null, null, null, null, null, null);
        ExceptionUtil.validateConditionDataNotFoundException(ValidateUtil.isSize(names, 1), "name not found", name, null);

        Name toBeChecked = names.get(0);

        // system structure
        if (toBeChecked.getSystemGroupUuid() != null) {
            // system group

            SystemGroup systemGroup = holderIRepositories.systemGroupRepository().findLatestNotDeletedByUuid(toBeChecked.getSystemGroupUuid().toString());
            if (systemGroup == null) {
                return Boolean.TRUE;
            }
        } else if (toBeChecked.getSystemUuid() != null) {
            // system
            // system group

            org.openepics.names.repository.model.System system = holderIRepositories.systemRepository().findLatestNotDeletedByUuid(toBeChecked.getSystemUuid().toString());
            if (system == null) {
                return Boolean.TRUE;
            }

            SystemGroup systemGroup = holderIRepositories.systemGroupRepository().findLatestNotDeletedByUuid(system.getParentUuid().toString());
            if (systemGroup == null) {
                return Boolean.TRUE;
            }
        } else if (toBeChecked.getSubsystemUuid() != null) {
            // subsystem
            // system
            // system group

            Subsystem subsystem = holderIRepositories.subsystemRepository().findLatestNotDeletedByUuid(toBeChecked.getSubsystemUuid().toString());
            if (subsystem == null) {
                return Boolean.TRUE;
            }

            org.openepics.names.repository.model.System system = holderIRepositories.systemRepository().findLatestNotDeletedByUuid(subsystem.getParentUuid().toString());
            if (system == null) {
                return Boolean.TRUE;
            }

            SystemGroup systemGroup = holderIRepositories.systemGroupRepository().findLatestNotDeletedByUuid(system.getParentUuid().toString());
            if (systemGroup == null) {
                return Boolean.TRUE;
            }
        }
        // device structure
        if (toBeChecked.getDeviceTypeUuid() != null) {
            // device type
            // device group
            // discipline

            DeviceType deviceType = holderIRepositories.deviceTypeRepository().findLatestNotDeletedByUuid(toBeChecked.getDeviceTypeUuid().toString());
            if (deviceType == null) {
                return Boolean.TRUE;
            }

            DeviceGroup deviceGroup = holderIRepositories.deviceGroupRepository().findLatestNotDeletedByUuid(deviceType.getParentUuid().toString());
            if (deviceGroup == null) {
                return Boolean.TRUE;
            }

            Discipline discipline = holderIRepositories.disciplineRepository().findLatestNotDeletedByUuid(deviceGroup.getParentUuid().toString());
            if (discipline == null) {
                return Boolean.TRUE;
            }
        }

        return Boolean.FALSE;
    }

    public Boolean isValidToCreateName(String name) {
        // validation outside method
        // validate data - not exists

        LOGGER.log(Level.FINE, () -> MessageFormat.format(TextUtil.DESCRIPTION_NAME_VALUE, TextUtil.IS_VALID_TO_CREATE_NAME, "name", name));

        // initiate holder of system and device structure content, for performance reasons
        //     note false to not include deleted entries
        HolderStructures holderStructures = new HolderStructures(holderIRepositories, false);

        ValidateNameElementUtil.validateNameDataCreate(name, namingConvention, holderRepositories, holderStructures);

        return Boolean.TRUE;
    }

    // ----------------------------------------------------------------------------------------------------

    public void validateNamesCreate(NameElementCommand nameElement) {
        validateNamesCreate(nameElement, new HolderStructures(holderIRepositories));
    }
    public void validateNamesCreate(NameElementCommand nameElement, HolderStructures holderStructures) {
        // validate name element
        //     input
        //         input itself
        //     data
        //         data itself
        //         relative other data

        // validate input
        ValidateNameElementUtil.validateNameElementInputCreate(nameElement);

        // validate data
        ValidateNameElementUtil.validateNameElementDataCreate(nameElement, namingConvention, holderIRepositories, nameRepository, holderStructures);
    }
    public void validateNamesCreate(List<NameElementCommand> nameElements) {
        // initiate holder of system and device structure content, for performance reasons
        HolderStructures holderStructures = new HolderStructures(holderIRepositories);

        for (NameElementCommand nameElement : nameElements) {
            validateNamesCreate(nameElement, holderStructures);
        }
    }
    public void validateNamesUpdate(NameElementCommand nameElement) {
        validateNamesUpdate(nameElement, new HolderStructures(holderIRepositories));
    }
    public void validateNamesUpdate(NameElementCommand nameElement, HolderStructures holderStructures) {
        // validate name element
        //     input
        //         input itself
        //     data
        //         data itself
        //         relative other data

        // validate input
        ValidateNameElementUtil.validateNameElementInputUpdate(nameElement);

        // validate data
        ValidateNameElementUtil.validateNameElementDataUpdate(nameElement, namingConvention, holderIRepositories, nameRepository, holderStructures);
    }
    public void validateNamesUpdate(List<NameElementCommand> nameElements) {
        // initiate holder of system and device structure content, for performance reasons
        HolderStructures holderStructures = new HolderStructures(holderIRepositories);

        for (NameElementCommand nameElement : nameElements) {
            validateNamesUpdate(nameElement, holderStructures);
        }
    }

    public void validateNamesDelete(NameElementCommand nameElement) {
        validateNamesDelete(nameElement, new HolderStructures(holderIRepositories));
    }
    public void validateNamesDelete(NameElementCommand nameElement, HolderStructures holderStructures) {
        // validate name element
        //     input
        //         input itself
        //     data
        //         data itself
        //         relative other data

        // validate input
        ValidateNameElementUtil.validateNameElementInputDelete(nameElement);

        // validate data
        ValidateNameElementUtil.validateNameElementDataDelete(nameElement, namingConvention, holderIRepositories, nameRepository, holderStructures);
    }
    public void validateNamesDelete(List<NameElementCommand> nameElements) {
        // initiate holder of system and device structure content, for performance reasons
        HolderStructures holderStructures = new HolderStructures(holderIRepositories);

        for (NameElementCommand nameElement : nameElements) {
            validateNamesDelete(nameElement, holderStructures);
        }
    }

    // ----------------------------------------------------------------------------------------------------

    @Transactional
    public List<NameElement> updateNames(List<NameElementCommand> nameElementCommands, String username) {
        // validation outside method
        // transaction
        //     for each name element
        //         attributes
        //         find
        //         update name to not latest
        //         find
        //         insert name to latest, not deleted, with data
        //         handle name element for updated name
        //     no notify
        //     return name elements for updated names
        //
        // attributes
        //     uuid, parentSystemStructure, parentDeviceStructure, index, description, comment

        // initiate holder of system and device structure content, for performance reasons
        HolderStructures holderStructures = new HolderStructures(holderIRepositories);

        Date when = new Date();
        final List<NameElement> updatedNameElements = Lists.newArrayList();
        for (NameElementCommand nameElementCommand : nameElementCommands) {
            // update not latest, not deleted
            // create latest, not deleted

            UUID uuid = nameElementCommand.getUuid();
            UUID parentSystemStructure = nameElementCommand.getParentSystemStructure();
            UUID parentDeviceStructure = nameElementCommand.getParentDeviceStructure();
            String index = nameElementCommand.getIndex();
            String description = nameElementCommand.getDescription();
            String comment = nameElementCommand.getComment();

            Name name = holderIRepositories.nameRepository().findLatestByUuid(uuid.toString());
            if (name == null) {
                continue;
            }

            // skip if name element has same content as name
            //     proceed without fail
            if (NameElementUtil.hasSameContent(nameElementCommand, name, holderIRepositories, holderStructures)) {
                continue;
            }

            // update
            name.setLatest(Boolean.FALSE);
            nameRepository.updateName(name);

            // find out system group, system, subsystem
            //     one of the three expected to be non-null, other two expected to be null
            SystemGroup systemGroup = holderIRepositories.systemGroupRepository().findLatestByUuid(parentSystemStructure.toString());
            System      system      = holderIRepositories.systemRepository().findLatestByUuid(parentSystemStructure.toString());
            Subsystem   subsystem   = holderIRepositories.subsystemRepository().findLatestByUuid(parentSystemStructure.toString());
            DeviceType  deviceType  = null;
            if (parentDeviceStructure != null) {
                deviceType  = holderIRepositories.deviceTypeRepository().findLatestByUuid(parentDeviceStructure.toString());
            }

            String derivedName = null;
            if (systemGroup != null) {
                derivedName = NameUtil.getName(systemGroup, deviceType, index, holderStructures);
            } else if (system != null) {
                derivedName = NameUtil.getName(system, deviceType, index, holderStructures);
            } else if (subsystem != null) {
                derivedName = NameUtil.getName(subsystem, deviceType, index, holderStructures);
            }

            // create
            name = new Name(uuid,
                    systemGroup != null ? systemGroup.getUuid() : null,
                    system != null ? system.getUuid() : null,
                    subsystem != null ? subsystem.getUuid() : null,
                    parentDeviceStructure,
                    index, derivedName, namingConvention.equivalenceClassRepresentative(derivedName),
                    description, Status.APPROVED, Boolean.TRUE, Boolean.FALSE,
                    when, username, comment);
            nameRepository.createName(name);

            NameElement updatedNameElement = NameElementUtil.getNameElement(name);
            updatedNameElements.add(updatedNameElement);

            if (LOGGER.isLoggable(Level.FINER)) {
                LOGGER.log(Level.FINER, MessageFormat.format(TextUtil.DESCRIPTION_NAME_VALUE, TextUtil.UPDATE_NAME, TextUtil.ELEMENT_IN, nameElementCommand));
                LOGGER.log(Level.FINER, MessageFormat.format(TextUtil.DESCRIPTION_NAME_VALUE, TextUtil.UPDATE_NAME, TextUtil.ELEMENT_OUT, updatedNameElement));
            }
        }

        LOGGER.log(Level.INFO,
                () -> MessageFormat.format(
                        TextUtil.DESCRIPTION_NUMBER_ELEMENTS_IN_OUT,
                        "Update names",
                        nameElementCommands.size(),
                        updatedNameElements.size()));
        return updatedNameElements;
    }

    @Transactional(propagation = Propagation.MANDATORY)
    public List<NameElement> updateNames(Structure previousStructure, Structure structure, String username) {
        // validation outside method
        // transaction
        //     support a current transaction, throw an exception if none exists
        //     find out names referenced by structure
        //         not for device group
        //         sg, sys, sys-sub, sg:di-dt-idx, sys:di-dt-idx, sys-sub:di-dt-idx
        //     prepare name element commands
        //     update names - re-calculate names
        //     return name elements for updated names

        List<NameElement> updatedNameElements = Lists.newArrayList();
        if (previousStructure != null
                && structure != null
                && !StringUtils.equals(structure.getMnemonic(), previousStructure.getMnemonic())
                && !StringUtils.isEmpty(structure.getMnemonic())) {
            List<Name> names = null;
            List<NameElementCommand> nameElements = Lists.newArrayList();
            UUID parentSystemStructure = null;
            if (previousStructure instanceof SystemGroup && structure instanceof SystemGroup) {
                // find
                // prepare
                names = holderIRepositories.nameRepository().findLatestBySystemGroupUuid(structure.getUuid().toString());
                for (Name name : names) {
                    nameElements.add(
                            new NameElementCommand(
                                    name.getUuid(), name.getSystemGroupUuid(), name.getDeviceTypeUuid(),
                                    name.getInstanceIndex(), name.getDescription(), UPDATE_AFTER_APPROVE_STRUCTURE_CHANGE));
                }
            } else if (previousStructure instanceof System && structure instanceof System) {
                // find
                // prepare
                names = holderIRepositories.nameRepository().findLatestBySystemUuid(structure.getUuid().toString());
                for (Name name : names) {
                    nameElements.add(
                            new NameElementCommand(
                                    name.getUuid(), name.getSystemUuid(), name.getDeviceTypeUuid(),
                                    name.getInstanceIndex(), name.getDescription(), UPDATE_AFTER_APPROVE_STRUCTURE_CHANGE));
                }

                names = holderIRepositories.nameRepository().findLatestBySystemUuidThroughSubsystem(structure.getUuid().toString());
                for (Name name : names) {
                    nameElements.add(
                            new NameElementCommand(
                                    name.getUuid(), name.getSubsystemUuid(), name.getDeviceTypeUuid(),
                                    name.getInstanceIndex(), name.getDescription(), UPDATE_AFTER_APPROVE_STRUCTURE_CHANGE));
                }
            } else if (previousStructure instanceof Subsystem && structure instanceof Subsystem) {
                // find
                // prepare
                names = holderIRepositories.nameRepository().findLatestBySubsystemUuid(structure.getUuid().toString());
                for (Name name : names) {
                    nameElements.add(
                            new NameElementCommand(
                                    name.getUuid(), name.getSubsystemUuid(), name.getDeviceTypeUuid(),
                                    name.getInstanceIndex(), name.getDescription(), UPDATE_AFTER_APPROVE_STRUCTURE_CHANGE));
                }
            } else if (previousStructure instanceof Discipline && structure instanceof Discipline) {
                // find
                // prepare
                names = holderIRepositories.nameRepository().findLatestByDisciplineUuidThroughDeviceType(structure.getUuid().toString());
                for (Name name : names) {
                    parentSystemStructure = name.getSubsystemUuid() != null
                            ? name.getSubsystemUuid()
                            : name.getSystemUuid() != null
                                    ? name.getSystemUuid()
                                    : name.getSystemGroupUuid() != null
                                            ? name.getSystemGroupUuid()
                                            : null;
                    nameElements.add(
                            new NameElementCommand(
                                    name.getUuid(), parentSystemStructure, name.getDeviceTypeUuid(),
                                    name.getInstanceIndex(), name.getDescription(), UPDATE_AFTER_APPROVE_STRUCTURE_CHANGE));
                }
            } else if (previousStructure instanceof DeviceType && structure instanceof DeviceType) {
                // find
                // prepare
                names = holderIRepositories.nameRepository().findLatestByDeviceTypeUuid(structure.getUuid().toString());
                for (Name name : names) {
                    parentSystemStructure = name.getSubsystemUuid() != null
                            ? name.getSubsystemUuid()
                            : name.getSystemUuid() != null
                                    ? name.getSystemUuid()
                                    : name.getSystemGroupUuid() != null
                                            ? name.getSystemGroupUuid()
                                            : null;
                    nameElements.add(
                            new NameElementCommand(
                                    name.getUuid(), parentSystemStructure, name.getDeviceTypeUuid(),
                                    name.getInstanceIndex(), name.getDescription(), UPDATE_AFTER_APPROVE_STRUCTURE_CHANGE));
                }
            }

            // update names
            //     use active transaction or, if not available, throw exception
            updatedNameElements = updateNames(nameElements, username);
        }
        return updatedNameElements;
    }

    // ----------------------------------------------------------------------------------------------------

    @Transactional
    public List<NameElement> deleteNames(List<NameElementCommand> nameElementCommands, String username) {
        // validation outside method
        // transaction
        //     for each name element
        //         attributes
        //         find
        //         update name to not latest
        //         insert name to latest, deleted
        //         handle name element for deleted name
        //     no notify
        //     return name elements for deleted names
        //
        // attributes
        //     uuid, comment

        Date when = new Date();
        final List<NameElement> deletedNameElements = Lists.newArrayList();
        for (NameElementCommand nameElementCommand : nameElementCommands) {
            UUID uuid = nameElementCommand.getUuid();
            String comment = nameElementCommand.getComment();

            Name name = holderIRepositories.nameRepository().findLatestByUuid(uuid.toString());
            if (name == null) {
                continue;
            }
            name.setLatest(Boolean.FALSE);
            nameRepository.updateName(name);

            name = new Name(uuid,
                    name.getSystemGroupUuid(),
                    name.getSystemUuid(),
                    name.getSubsystemUuid(),
                    name.getDeviceTypeUuid(),
                    name.getInstanceIndex(), name.getConventionName(), name.getConventionNameEquivalence(),
                    name.getDescription(), Status.APPROVED, Boolean.TRUE, Boolean.TRUE,
                    when, username, comment);
            nameRepository.createName(name);

            NameElement deletedNameElement = NameElementUtil.getNameElement(name);
            deletedNameElements.add(deletedNameElement);

            if (LOGGER.isLoggable(Level.FINER)) {
                LOGGER.log(Level.FINER, MessageFormat.format(TextUtil.DESCRIPTION_NAME_VALUE, TextUtil.DELETE_NAME, TextUtil.ELEMENT_IN, nameElementCommand));
                LOGGER.log(Level.FINER, MessageFormat.format(TextUtil.DESCRIPTION_NAME_VALUE, TextUtil.DELETE_NAME, TextUtil.ELEMENT_OUT, deletedNameElement));
            }
        }

        LOGGER.log(Level.INFO,
                () -> MessageFormat.format(
                        TextUtil.DESCRIPTION_NUMBER_ELEMENTS_IN_OUT,
                        "Delete names",
                        nameElementCommands.size(),
                        deletedNameElements.size()));
        return deletedNameElements;
    }

}