class MetadataExportServiceImpl extends AbstractMetadataService implements MetadataExportService {

    @Nonnull
    @Override
    @Transactional( readOnly = true, isolation = Isolation.REPEATABLE_READ )
    public JsonNode exp( @Nonnull MetadataExportParams params )
    {
        final TypedMetadataObjectContainer typedContainer = new TypedMetadataObjectContainer();

        final List<MappedTrackerProgram> trackerPrograms = params.getTrackerProgramIds().isEmpty() ?
            trackerProgramRepository.findAll() : trackerProgramRepository.findAllById( params.getTrackerProgramIds() );
        typedContainer.addObjects( trackerPrograms );

        if ( !trackerPrograms.isEmpty() )
        {
            typedContainer.addObjects( programStageRuleRepository.findAllByProgram( trackerPrograms ) );

            if ( params.isIncludeTrackedEntities() )
            {
                typedContainer.addObjects( trackerPrograms.stream().map( MappedTrackerProgram::getTrackedEntityRule )
                    .collect( Collectors.toList() ) );
            }
        }

        if ( params.isIncludeResourceMappings() )
        {
            processFhirResourceTypes( typedContainer, params );
        }

        final Set<Class<? extends Metadata>> processedTypes = new HashSet<>();
        final Map<Class<? extends Metadata>, JsonNode> typedNodes = new HashMap<>();

        if ( !params.isIncludeTrackedEntities() )
        {
            processedTypes.add( MappedTrackedEntity.class );
            processedTypes.add( TrackedEntityRule.class );
        }

        if ( !params.isIncludeResourceMappings() )
        {
            processedTypes.add( FhirResourceMapping.class );
        }

        do
        {
            final Set<Class<? extends Metadata>> remainingTypes = new HashSet<>( typedContainer.getTypes() );
            remainingTypes.removeAll( processedTypes );

            remainingTypes.stream().sorted( Comparator.comparing( MetadataExportField::getByClass ).reversed() ).forEach( type -> {
                final ObjectWriter objectWriter = mapper.writerFor( type ).with( new SimpleFilterProvider()
                    .addFilter( SecuredPropertyFilter.FILTER_NAME, emptyPropertyFilter )
                    .addFilter( AdapterBeanPropertyFilter.FILTER_NAME,
                        new MetadataExportPropertyFilter( params, repositoryNames.keySet(), typedContainer,
                            metadataExportDependencyResolvers ) ) );

                final ArrayNode nodes = JsonNodeFactory.instance.arrayNode();
                new ArrayList<>( typedContainer.getContainer( type ).getObjects() )
                    .forEach( o -> nodes.add( valueToTree( o, objectWriter ) ) );

                if ( typedNodes.put( type, nodes ) != null )
                {
                    throw new IllegalStateException( "Type " + type + " has already been processed." );
                }

                processedTypes.add( type );
            } );
        }
        while ( !processedTypes.containsAll( typedContainer.getTypes() ) );

        final ObjectNode rootNode = JsonNodeFactory.instance.objectNode();
        rootNode.set( MetadataVersionInfo.VERSION_INFO_FIELD_NAME, mapper.valueToTree( createVersionInfo() ) );
        typedNodes.forEach( ( type, node ) -> rootNode.set( getMetadataPluralName( type ), node ) );

        return rootNode;
    }

    public  MetadataExportServiceImpl( @Nonnull MappedTrackerProgramRepository trackerProgramRepository,
        @Nonnull ProgramStageRuleRepository programStageRuleRepository,
        @Nonnull FhirResourceMappingRepository fhirResourceMappingRepository,
        @Nonnull List<? extends MetadataRepository<? extends Metadata>> repositories,
        @Nonnull List<MetadataExportDependencyResolver> metadataExportDependencyResolvers,
        @Nullable BuildProperties buildProperties );

}