org.kaaproject.kaa.server.common.dao.service.CtlServiceImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.kaaproject.kaa.server.common.dao.service.CtlServiceImpl.java

Source

/*
 * Copyright 2014-2016 CyberVision, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.kaaproject.kaa.server.common.dao.service;

import static org.apache.commons.lang.StringUtils.isBlank;
import static org.kaaproject.kaa.common.dto.ctl.CTLSchemaScopeDto.SYSTEM;
import static org.kaaproject.kaa.server.common.dao.impl.DaoUtil.convertDtoList;
import static org.kaaproject.kaa.server.common.dao.impl.DaoUtil.getDto;
import static org.kaaproject.kaa.server.common.dao.impl.DaoUtil.getStringFromFile;
import static org.kaaproject.kaa.server.common.dao.service.Validator.validateObject;
import static org.kaaproject.kaa.server.common.dao.service.Validator.validateSqlId;
import static org.kaaproject.kaa.server.common.dao.service.Validator.validateString;

import org.apache.avro.Schema;
import org.apache.avro.generic.GenericRecord;
import org.apache.commons.lang.Validate;
import org.codehaus.jackson.JsonNode;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.node.ArrayNode;
import org.codehaus.jackson.node.ObjectNode;
import org.hibernate.LockMode;
import org.hibernate.LockOptions;
import org.kaaproject.kaa.common.avro.GenericAvroConverter;
import org.kaaproject.kaa.common.dto.ctl.CTLSchemaDto;
import org.kaaproject.kaa.common.dto.ctl.CTLSchemaScopeDto;
import org.kaaproject.kaa.common.dto.ctl.CtlSchemaMetaInfoDto;
import org.kaaproject.kaa.common.dto.file.FileData;
import org.kaaproject.kaa.server.common.core.algorithms.generation.ConfigurationGenerationException;
import org.kaaproject.kaa.server.common.core.algorithms.generation.DefaultRecordGenerationAlgorithm;
import org.kaaproject.kaa.server.common.core.algorithms.generation.DefaultRecordGenerationAlgorithmImpl;
import org.kaaproject.kaa.server.common.core.configuration.RawData;
import org.kaaproject.kaa.server.common.core.configuration.RawDataFactory;
import org.kaaproject.kaa.server.common.core.schema.RawSchema;
import org.kaaproject.kaa.server.common.dao.CtlService;
import org.kaaproject.kaa.server.common.dao.exception.DatabaseProcessingException;
import org.kaaproject.kaa.server.common.dao.exception.IncorrectParameterException;
import org.kaaproject.kaa.server.common.dao.impl.CtlSchemaDao;
import org.kaaproject.kaa.server.common.dao.impl.CtlSchemaMetaInfoDao;
import org.kaaproject.kaa.server.common.dao.impl.DaoUtil;
import org.kaaproject.kaa.server.common.dao.model.sql.CtlSchema;
import org.kaaproject.kaa.server.common.dao.model.sql.CtlSchemaMetaInfo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

@Service
@Transactional
public class CtlServiceImpl implements CtlService {

    private static final String JSON = "application/json";
    private static final String ZIP = "application/zip";
    private static final String VERSION = "version";
    private static final String FQN = "fqn";
    private static final Logger LOG = LoggerFactory.getLogger(CtlServiceImpl.class);
    private static final String DEPENDENCIES = "dependencies";
    private static final String DEFAULT_SYSTEM_EMPTY_SCHEMA_FILE = "/default_system_empty_schema" + ".avsc";
    /**
     * A template for naming exported CTL schemas.
     *
     * @see #shallowExport(CTLSchemaDto)
     * @see #flatExport(CTLSchemaDto)
     */
    private static final String CTL_EXPORT_TEMPLATE = "{0}.v{1}.avsc";
    /**
     * The name of the archive to put exported CTL schemas into.
     *
     * @see #deepExport(CTLSchemaDto)
     */
    private static final String CTL_EXPORT_ZIP_NAME = "schemas.zip";
    /**
     * Used to format CTL schema body.
     */
    private static final ObjectMapper FORMATTER = new ObjectMapper();
    private final LockOptions lockOptions = new LockOptions(LockMode.PESSIMISTIC_WRITE);
    @Autowired
    private CtlSchemaDao<CtlSchema> ctlSchemaDao;

    @Autowired
    private CtlSchemaMetaInfoDao<CtlSchemaMetaInfo> ctlSchemaMetaInfoDao;

    @Override
    public CTLSchemaDto getOrCreateEmptySystemSchema(String createdUsername) {
        CTLSchemaDto ctlSchema = findCtlSchemaByFqnAndVerAndTenantIdAndApplicationId(
                DEFAULT_SYSTEM_EMPTY_SCHEMA_FQN, DEFAULT_SYSTEM_EMPTY_SCHEMA_VERSION, null, null);
        if (ctlSchema == null) {
            ctlSchema = new CTLSchemaDto();
            CtlSchemaMetaInfoDto metaInfo = new CtlSchemaMetaInfoDto(DEFAULT_SYSTEM_EMPTY_SCHEMA_FQN);
            ctlSchema.setMetaInfo(metaInfo);
            ctlSchema.setVersion(DEFAULT_SYSTEM_EMPTY_SCHEMA_VERSION);
            ctlSchema.setCreatedUsername(createdUsername);
            ctlSchema.setDependencySet(new HashSet<CTLSchemaDto>());
            String body = getStringFromFile(DEFAULT_SYSTEM_EMPTY_SCHEMA_FILE, CtlServiceImpl.class);
            if (!body.isEmpty()) {
                ctlSchema.setBody(body);
            } else {
                throw new RuntimeException("Can't read default system schema."); // NOSONAR
            }
            ctlSchema = saveCtlSchema(ctlSchema);
        }
        return ctlSchema;
    }

    @Override
    public CTLSchemaDto saveCtlSchema(CTLSchemaDto unSavedSchema) {
        validateCtlSchemaObject(unSavedSchema);
        if (unSavedSchema.getDefaultRecord() == null) {
            unSavedSchema = generateDefaultRecord(unSavedSchema);
        } else {
            validateDefaultRecord(unSavedSchema);
        }
        if (isBlank(unSavedSchema.getId())) {
            CtlSchemaMetaInfoDto metaInfo = unSavedSchema.getMetaInfo();
            CTLSchemaDto dto;
            synchronized (this) {
                List<CtlSchemaMetaInfo> existingFqns = ctlSchemaMetaInfoDao.findExistingFqns(metaInfo.getFqn(),
                        metaInfo.getTenantId(), metaInfo.getApplicationId());
                if (existingFqns != null && !existingFqns.isEmpty()) {
                    throw new DatabaseProcessingException("Can't save common type due to an FQN conflict.");
                }
                metaInfo.setId(null);
                CtlSchemaMetaInfo uniqueMetaInfo = ctlSchemaMetaInfoDao.save(new CtlSchemaMetaInfo(metaInfo));
                ctlSchemaMetaInfoDao.lockRequest(lockOptions).setScope(true).lock(uniqueMetaInfo);
                CtlSchema ctlSchema = new CtlSchema(unSavedSchema);
                ctlSchema.setMetaInfo(uniqueMetaInfo);
                ctlSchema.setCreatedTime(System.currentTimeMillis());
                ctlSchemaMetaInfoDao.refresh(uniqueMetaInfo);
                try {
                    dto = getDto(ctlSchemaDao.save(ctlSchema, true));
                } catch (DataIntegrityViolationException ex) {
                    throw new DatabaseProcessingException(
                            "Can't save common type: such FQN and version " + "already exist.");
                } catch (Exception ex) {
                    throw new DatabaseProcessingException(ex);
                }
            }
            return dto;
        } else {
            return updateCtlSchema(unSavedSchema);
        }
    }

    private void validateDefaultRecord(CTLSchemaDto unSavedSchema) {
        try {
            String schemaBody = flatExportAsString(unSavedSchema);
            GenericAvroConverter<GenericRecord> converter = new GenericAvroConverter<>(schemaBody);
            converter.decodeJson(unSavedSchema.getDefaultRecord());
        } catch (IOException | RuntimeException ex) {
            LOG.error("Invalid default record for CTL schema with body: {}", unSavedSchema.getBody(), ex);
            throw new RuntimeException("An unexpected exception occured: " + ex.toString());
        }
    }

    private CTLSchemaDto generateDefaultRecord(CTLSchemaDto unSavedSchema) {
        try {
            String schemaBody = flatExportAsString(unSavedSchema);
            LOG.debug("Generating default record for flat schema: {}", schemaBody);
            RawSchema dataSchema = new RawSchema(schemaBody);
            DefaultRecordGenerationAlgorithm<RawData> dataProcessor = new DefaultRecordGenerationAlgorithmImpl<RawSchema, RawData>(
                    dataSchema, new RawDataFactory());
            unSavedSchema.setDefaultRecord(dataProcessor.getRootData().getRawData());
            return unSavedSchema;
        } catch (StackOverflowError ex) {
            LOG.error(
                    "Failed to generate default record. An endless recursion is detected. CTL schema " + "body: {}",
                    unSavedSchema.getBody(), ex);
            throw new RuntimeException("Unable to generate default record. An endless recursion is " + "detected! "
                    + "Please check non-optional references to nested types.");
        } catch (ConfigurationGenerationException | IOException | RuntimeException ex) {
            LOG.error("Failed to generate default record for CTL schema with body: {}", unSavedSchema.getBody(),
                    ex);
            throw new RuntimeException("An unexpected exception occured: " + ex.toString());
        }
    }

    @Override
    public CTLSchemaDto updateCtlSchema(CTLSchemaDto ctlSchema) {
        validateCtlSchemaObject(ctlSchema);
        LOG.debug("Update ctl schema with id [{}]", ctlSchema.getId());
        CtlSchema schema = ctlSchemaDao.findById(ctlSchema.getId());
        if (schema != null) {
            synchronized (this) {
                if (ctlSchema.getVersion() != schema.getVersion()) {
                    throw new DatabaseProcessingException(
                            "Can't change version of existing common type " + "version.");
                }
                CtlSchemaMetaInfo metaInfo = schema.getMetaInfo();
                if (!ctlSchema.getMetaInfo().equals(metaInfo.toDto())) {
                    throw new DatabaseProcessingException(
                            "Can't update scope of existing common type " + "version within update procedure.");
                }
                ctlSchemaMetaInfoDao.lockRequest(lockOptions).setScope(true).lock(metaInfo);
                schema.update(ctlSchema);
                return DaoUtil.getDto(ctlSchemaDao.save(schema, true));
            }
        } else {
            throw new DatabaseProcessingException("Can't find common type version by id.");
        }
    }

    @Override
    public CtlSchemaMetaInfoDto updateCtlSchemaMetaInfoScope(CtlSchemaMetaInfoDto ctlSchemaMetaInfo) {
        validateObject(ctlSchemaMetaInfo, "Incorrect ctl schema meta info object");
        LOG.debug("Update ctl schema meta info scope with id [{}]", ctlSchemaMetaInfo.getId());
        CtlSchemaMetaInfo schemaMetaInfo = ctlSchemaMetaInfoDao.findById(ctlSchemaMetaInfo.getId());
        if (schemaMetaInfo != null) {
            synchronized (this) {
                ctlSchemaMetaInfoDao.lockRequest(lockOptions).setScope(true).lock(schemaMetaInfo);
                if (checkScopeUpdate(ctlSchemaMetaInfo, schemaMetaInfo.toDto())) {
                    List<CtlSchemaMetaInfo> others = ctlSchemaMetaInfoDao.findOthersByFqnAndTenantId(
                            ctlSchemaMetaInfo.getFqn(), ctlSchemaMetaInfo.getTenantId(), ctlSchemaMetaInfo.getId());
                    if (others != null && !others.isEmpty()) {
                        throw new DatabaseProcessingException(
                                "Can't update scope of the common type due to " + "an FQN conflict.");
                    }
                    schemaMetaInfo = ctlSchemaMetaInfoDao.updateScope(new CtlSchemaMetaInfo(ctlSchemaMetaInfo));
                }
                return DaoUtil.getDto(schemaMetaInfo);
            }
        } else {
            throw new DatabaseProcessingException("Can't find common type by id.");
        }
    }

    @Override
    public List<CtlSchemaMetaInfoDto> findSiblingsByFqnTenantIdAndApplicationId(String fqn, String tenantId,
            String applicationId) {
        if (isBlank(fqn)) {
            throw new IncorrectParameterException("Incorrect parameters for ctl schema request.");
        }
        LOG.debug("Find sibling ctl schemas by fqn {}, tenant id {} and application id {}", fqn, tenantId,
                applicationId);
        return convertDtoList(
                ctlSchemaMetaInfoDao.findSiblingsByFqnTenantIdAndApplicationId(fqn, tenantId, applicationId));
    }

    private boolean checkScopeUpdate(CtlSchemaMetaInfoDto newSchemaMetaInfo,
            CtlSchemaMetaInfoDto prevSchemaMetaInfo) {
        if (!newSchemaMetaInfo.equals(prevSchemaMetaInfo)) {
            if (isBlank(newSchemaMetaInfo.getFqn())) {
                throw new DatabaseProcessingException("FQN can't be empty.");
            }
            if (!newSchemaMetaInfo.getFqn().equals(prevSchemaMetaInfo.getFqn())) {
                throw new DatabaseProcessingException("Can't change FQN of the existing common type.");
            }
            CTLSchemaScopeDto newScope = newSchemaMetaInfo.getScope();
            CTLSchemaScopeDto prevScope = prevSchemaMetaInfo.getScope();
            if (newScope != prevScope) {
                if (SYSTEM.equals(newScope)) {
                    throw new DatabaseProcessingException("Can't update scope to system.");
                }
                if (newScope.getLevel() > prevScope.getLevel()) {
                    throw new DatabaseProcessingException("Can't update scope to lower.");
                }
            }
            if (!isBlank(newSchemaMetaInfo.getTenantId())
                    && !newSchemaMetaInfo.getTenantId().equals(prevSchemaMetaInfo.getTenantId())) {
                throw new DatabaseProcessingException(
                        "Can't change tenant reference for the existing " + "common type.");
            }
            if (!isBlank(newSchemaMetaInfo.getApplicationId())
                    && !newSchemaMetaInfo.getApplicationId().equals(prevSchemaMetaInfo.getApplicationId())) {
                throw new DatabaseProcessingException(
                        "Can't change application reference for the " + "existing common type.");
            }
            return true;
        }
        return false;
    }

    @Override
    public void removeCtlSchemaByFqnAndVerAndTenantIdAndApplicationId(String fqn, Integer version, String tenantId,
            String applicationId) {
        if (isBlank(fqn) || version == null) {
            throw new IncorrectParameterException("Incorrect parameters for ctl schema request.");
        }
        LOG.debug("Remove ctl schema by fqn {} version {}, tenant id {} and application id {}", fqn, version,
                tenantId, applicationId);
        CtlSchema ctlSchema = ctlSchemaDao.findByFqnAndVerAndTenantIdAndApplicationId(fqn, version, tenantId,
                applicationId);
        if (ctlSchema != null) {
            List<CtlSchema> dependsList = ctlSchemaDao.findDependentSchemas(ctlSchema.getStringId());
            if (dependsList.isEmpty()) {
                synchronized (this) {
                    CtlSchemaMetaInfo metaInfo = ctlSchema.getMetaInfo();
                    ctlSchemaMetaInfoDao.lockRequest(lockOptions).setScope(true).lock(metaInfo);
                    try {
                        ctlSchemaDao.removeById(ctlSchema.getStringId());
                        List<CtlSchema> schemas = ctlSchemaDao.findAllByMetaInfoId(metaInfo.getStringId());
                        if (schemas == null || schemas.isEmpty()) {
                            ctlSchemaMetaInfoDao.removeById(metaInfo.getStringId());
                        }
                    } catch (DataIntegrityViolationException ex) {
                        throw new DatabaseProcessingException("Common type version can't be deleted because "
                                + "it is referenced by system modules.");
                    }
                }
            } else {
                throw new DatabaseProcessingException(
                        "Common type version can't be deleted because it is" + " referenced by other types.");
            }
        }
    }

    @Override
    public CTLSchemaDto findCtlSchemaById(String schemaId) {
        validateSqlId(schemaId, "Incorrect schema id for ctl request " + schemaId);
        LOG.debug("Find ctl schema by id [{}]", schemaId);
        return DaoUtil.getDto(ctlSchemaDao.findById(schemaId));
    }

    @Override
    public CTLSchemaDto findCtlSchemaByFqnAndVerAndTenantIdAndApplicationId(String fqn, Integer version,
            String tenantId, String applicationId) {
        if (isBlank(fqn) || version == null) {
            throw new IncorrectParameterException("Incorrect parameters for ctl schema request.");
        }
        LOG.debug("Find ctl schema by fqn {} version {}, tenant id {} and application id {}", fqn, version,
                tenantId, applicationId);
        return DaoUtil.getDto(
                ctlSchemaDao.findByFqnAndVerAndTenantIdAndApplicationId(fqn, version, tenantId, applicationId));
    }

    @Override
    public CTLSchemaDto findByMetaInfoIdAndVer(String metaInfoId, Integer version) {
        if (isBlank(metaInfoId) || version == null) {
            throw new IncorrectParameterException("Incorrect parameters for ctl schema request.");
        }
        LOG.debug("Find ctl schema by meta info id {} and version {}", metaInfoId, version);
        return DaoUtil.getDto(ctlSchemaDao.findByMetaInfoIdAndVer(metaInfoId, version));
    }

    @Override
    public CTLSchemaDto findAnyCtlSchemaByFqnAndVerAndTenantIdAndApplicationId(String fqn, Integer version,
            String tenantId, String applicationId) {
        if (isBlank(fqn) || version == null) {
            throw new IncorrectParameterException("Incorrect parameters for ctl schema request.");
        }
        LOG.debug("Find any ctl schema by fqn {} version {}, tenant id {} and application id {}", fqn, version,
                tenantId, applicationId);
        return DaoUtil.getDto(
                ctlSchemaDao.findAnyByFqnAndVerAndTenantIdAndApplicationId(fqn, version, tenantId, applicationId));
    }

    @Override
    public List<CTLSchemaDto> findSystemCtlSchemas() {
        LOG.debug("Find system ctl schemas");
        return convertDtoList(ctlSchemaDao.findSystemSchemas());
    }

    @Override
    public List<CtlSchemaMetaInfoDto> findSystemCtlSchemasMetaInfo() {
        LOG.debug("Find system ctl schemas");
        return getMetaInfoFromCtlSchema(ctlSchemaDao.findSystemSchemas());
    }

    @Override
    public List<CtlSchemaMetaInfoDto> findAvailableCtlSchemasMetaInfoForTenant(String tenantId) {
        LOG.debug("Find system and tenant scopes ctl schemas by tenant id {}", tenantId);
        return getMetaInfoFromCtlSchema(ctlSchemaDao.findAvailableSchemasForTenant(tenantId));
    }

    @Override
    public List<CtlSchemaMetaInfoDto> findAvailableCtlSchemasMetaInfoForApplication(String tenantId,
            String applicationId) {
        LOG.debug("Find system, tenant and application scopes ctl schemas by application id {}", applicationId);
        return getMetaInfoFromCtlSchema(ctlSchemaDao.findAvailableSchemasForApplication(tenantId, applicationId));
    }

    @Override
    public CTLSchemaDto findLatestCtlSchemaByFqnAndTenantIdAndApplicationId(String fqn, String tenantId,
            String applicationId) {
        validateString(fqn, "Incorrect fqn for ctl schema request.");
        LOG.debug("Find latest ctl schema by fqn {}, tenantId {} and applicationId {}", fqn, tenantId,
                applicationId);
        return DaoUtil
                .getDto(ctlSchemaDao.findLatestByFqnAndTenantIdAndApplicationId(fqn, tenantId, applicationId));
    }

    @Override
    public CTLSchemaDto findLatestByMetaInfoId(String metaInfoId) {
        validateString(metaInfoId, "Incorrect meta info id for ctl schema request.");
        LOG.debug("Find latest ctl schema by meta info id {}", metaInfoId);
        return DaoUtil.getDto(ctlSchemaDao.findLatestByMetaInfoId(metaInfoId));
    }

    @Override
    public List<CTLSchemaDto> findAllCtlSchemasByFqnAndTenantIdAndApplicationId(String fqn, String tenantId,
            String applicationId) {
        validateString(fqn, "Incorrect fqn for ctl schema request.");
        LOG.debug("Find all ctl schemas by fqn {}, tenantId {} and applicationId {}", fqn, tenantId, applicationId);
        return convertDtoList(ctlSchemaDao.findAllByFqnAndTenantIdAndApplicationId(fqn, tenantId, applicationId));
    }

    @Override
    public List<CTLSchemaDto> findCtlSchemas() {
        LOG.debug("Find all ctl schemas");
        return convertDtoList(ctlSchemaDao.find());
    }

    @Override
    public List<CTLSchemaDto> findCtlSchemaDependents(String schemaId) {
        validateSqlId(schemaId, "Incorrect schema id for ctl schema request.");
        LOG.debug("Find dependents schemas for schema with id [{}]", schemaId);
        List<CTLSchemaDto> list = Collections.emptyList();
        CtlSchema schemaDto = ctlSchemaDao.findById(schemaId);
        if (schemaDto != null) {
            list = convertDtoList(ctlSchemaDao.findDependentSchemas(schemaDto.getStringId()));
        }
        return list;
    }

    @Override
    public List<CTLSchemaDto> findCtlSchemaDependents(String fqn, Integer version, String tenantId,
            String applicationId) {
        if (isBlank(fqn) || version == null) {
            throw new IncorrectParameterException("Incorrect parameters for ctl schema request.");
        }
        LOG.debug(
                "Find dependents schemas for schema with fqn {} version {}, tenantId {} and " + "applicationId ()",
                fqn, version, tenantId, applicationId);
        List<CTLSchemaDto> schemas = Collections.emptyList();
        CtlSchema schema = ctlSchemaDao.findByFqnAndVerAndTenantIdAndApplicationId(fqn, version, tenantId,
                applicationId);
        if (schema != null) {
            schemas = convertDtoList(ctlSchemaDao.findDependentSchemas(schema.getStringId()));
        }
        return schemas;
    }

    private void validateCtlSchemaObject(CTLSchemaDto ctlSchema) {
        validateObject(ctlSchema, "Incorrect ctl schema object");
        CtlSchemaMetaInfoDto metaInfo = ctlSchema.getMetaInfo();
        if (metaInfo == null) {
            throw new RuntimeException(
                    "Incorrect ctl schema object. CtlSchemaMetaInfoDto is mandatory " + "information.");
        } else {
            if (isBlank(metaInfo.getFqn()) || ctlSchema.getVersion() == null) {
                throw new RuntimeException("Incorrect CTL schema, please add correct version and fqn.");
            }
        }
    }

    private List<CtlSchemaMetaInfoDto> getMetaInfoFromCtlSchema(List<CtlSchema> schemas) {
        Map<String, CtlSchemaMetaInfoDto> metaInfoMap = new HashMap<>();
        if (!schemas.isEmpty()) {
            for (CtlSchema schema : schemas) {
                String metaInfoId = schema.getMetaInfo().getStringId();
                CtlSchemaMetaInfoDto metaInfoDto = metaInfoMap.get(metaInfoId);
                if (metaInfoDto == null) {
                    metaInfoDto = getDto(schema.getMetaInfo());
                    metaInfoMap.put(metaInfoId, metaInfoDto);
                }
                List<Integer> versions = metaInfoDto.getVersions();
                if (versions == null) {
                    versions = new ArrayList<>();
                    metaInfoDto.setVersions(versions);
                }
                versions.add(schema.getVersion());
            }
        }
        List<CtlSchemaMetaInfoDto> result = new ArrayList<>(metaInfoMap.values());
        return result;
    }

    @Override
    public FileData shallowExport(CTLSchemaDto schema) {
        try {
            FileData result = new FileData();
            result.setContentType(JSON);
            result.setFileName(
                    MessageFormat.format(CTL_EXPORT_TEMPLATE, schema.getMetaInfo().getFqn(), schema.getVersion()));

            // Format schema body
            Object json = FORMATTER.readValue(schema.getBody(), Object.class);
            result.setFileData(FORMATTER.writerWithDefaultPrettyPrinter().writeValueAsString(json).getBytes());

            return result;
        } catch (Exception cause) {
            throw new RuntimeException("An unexpected exception occured: " + cause.toString());
        }
    }

    @Override
    public Schema flatExportAsSchema(CTLSchemaDto schema) {
        try {
            return this.parseDependencies(schema, new Schema.Parser());
        } catch (Exception cause) {
            LOG.error("Unable to export CTL schema as flat: {}", schema, cause);
            throw new RuntimeException("An unexpected exception occured: " + cause.toString());
        }
    }

    @Override
    public String flatExportAsString(CTLSchemaDto schema) {
        return flatExportAsSchema(schema).toString();
    }

    @Override
    public FileData flatExport(CTLSchemaDto schema) {
        try {
            FileData result = new FileData();
            result.setContentType(JSON);
            result.setFileName(
                    MessageFormat.format(CTL_EXPORT_TEMPLATE, schema.getMetaInfo().getFqn(), schema.getVersion()));

            // Get schema body
            String body = flatExportAsString(schema);

            // Format schema body
            Object json = FORMATTER.readValue(body, Object.class);
            result.setFileData(FORMATTER.writerWithDefaultPrettyPrinter().writeValueAsString(json).getBytes());

            return result;
        } catch (Exception cause) {
            throw new RuntimeException("An unexpected exception occured: " + cause.toString());
        }
    }

    @Override
    public FileData deepExport(CTLSchemaDto schema) {
        try {
            ByteArrayOutputStream content = new ByteArrayOutputStream();
            ZipOutputStream out = new ZipOutputStream(content);
            List<FileData> files = this.recursiveShallowExport(new ArrayList<FileData>(), schema);
            for (FileData file : files) {
                out.putNextEntry(new ZipEntry(file.getFileName()));
                out.write(file.getFileData());
                out.closeEntry();
            }
            out.close();

            FileData result = new FileData();
            result.setContentType(ZIP);
            result.setFileName(CTL_EXPORT_ZIP_NAME);
            result.setFileData(content.toByteArray());
            return result;
        } catch (Exception cause) {
            throw new RuntimeException("An unexpected exception occured: " + cause.toString());
        }
    }

    private Schema parseDependencies(CTLSchemaDto schema, final Schema.Parser parser) throws Exception {
        if (schema.getDependencySet() != null) {
            for (CTLSchemaDto dependency : schema.getDependencySet()) {
                this.parseDependencies(dependency, parser);
            }
        }
        ObjectNode object = new ObjectMapper().readValue(schema.getBody(), ObjectNode.class);
        object.remove(DEPENDENCIES);
        String body = new ObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(object);
        return parser.parse(body);
    }

    private List<FileData> recursiveShallowExport(List<FileData> files, CTLSchemaDto parent) throws Exception {
        files.add(this.shallowExport(parent));
        ObjectNode object = new ObjectMapper().readValue(parent.getBody(), ObjectNode.class);
        ArrayNode dependencies = (ArrayNode) object.get(DEPENDENCIES);
        if (dependencies != null) {
            for (JsonNode node : dependencies) {
                ObjectNode dependency = (ObjectNode) node;
                String fqn = dependency.get(FQN).getTextValue();
                Integer version = dependency.get(VERSION).getIntValue();
                CTLSchemaDto child = this.findAnyCtlSchemaByFqnAndVerAndTenantIdAndApplicationId(fqn, version,
                        parent.getMetaInfo().getTenantId(), parent.getMetaInfo().getApplicationId());
                Validate.notNull(child, MessageFormat.format("The dependency [{0}] was not found!", fqn));
                this.recursiveShallowExport(files, child);
            }
        }
        return files;
    }
}