# Copyright 1999-2012. Parallels IP Holdings GmbH. All Rights Reserved.
package DumpComposer;

use strict;
use XmlNode;
use Logging;
use XmlLogger;
use Agent;
use MigrationDumpStatus;
use CommonXmlNodes;
use CustomLogging;
use Storage::FileNameCreator;
use BranchTracker;
use ConfixxGuidGenerator;
use Dumper;

#
# Constants
#
my $DUMP_FILE_NAME = 'dump.xml';
my $STATUS_FILE_NAME = 'dumping-status.xml';
my $CONTENT_LIST_FILE_NAME = 'content-list.xml';
my $MIGRATION_RESULT_FILE_NAME = 'migration.result';

#
# package-local objects
#

my $dumpStatus = undef;

#
# BEGIN Interface methods
#

#
# Pass undef for all $resellersRef, $clientsRef and $domainsRef to make 'dump-all'
# undef means that all objects of a particular type should be dumped
#
sub makeDump {
  my ($workDir, $resellersRef, $clientsRef, $domainsRef, $options, $doNotWrite) = @_;

  Logging::setXmlLogging();

  Agent::setWorkDir($workDir);

  my @resellers = defined($resellersRef) ? @$resellersRef : _getAllResellers();
  my @clients = defined($clientsRef) ? @$clientsRef : _getAllClients();
  my @domains = defined($domainsRef) ? @$domainsRef : _getAllDomains();

  my $statusFile = "$workDir/$STATUS_FILE_NAME";
  $dumpStatus = MigrationDumpStatus->new($statusFile);
  $dumpStatus->start(scalar(@resellers) + scalar(@clients), scalar(@domains));

  my %selectedObjects = (
    'resellers' => { _hashSet(@resellers) },
    'clients' => { _hashSet(@clients) },
    'domains' => { _hashSet(@domains) }
  );

  my $adminXmlNode = _walkTree(
    _createFullTree(),
    my $nodeFunctions = {
      'admin' =>    \&_selectionDumpComposeAdminNode,
      'reseller' => \&_selectionDumpProcessReseller,
      'client' =>   \&_selectionDumpProcessClient,
      'domain' =>   sub {},
    },
    \%selectedObjects, $options
  );

  my $migrationDumpNode = _createMigrationDumpNode(
    $options->{'configOnly'},
    $options->{'dumpServerSettings'}, 
    $adminXmlNode 
  );

  unless ($doNotWrite) {
	  unless (_serializeXmlNode($migrationDumpNode,"$workDir/$DUMP_FILE_NAME")) {
		Logging::error("Dump file '$DUMP_FILE_NAME' is not saved.");
		return 0;
	  }

	  Logging::serializeXmlLog(AgentConfig::cwd() . '/'. $MIGRATION_RESULT_FILE_NAME);
	  $dumpStatus->finish();
	}

  return 1;
}

# Shallow dump ("-da -nc" command line arguments)
sub makeShallowDump {
  my ($workDir, $dumpServerSettings) = @_;

  Agent::setWorkDir($workDir);

  my @xmlNodes = _walkTree(
    _createFullTree(),
    my $nodeFunctions = {
      'admin' =>    \&_shallowDumpAdminNode,
      'reseller' => \&_shallowDumpResellerNode,
      'client' =>   \&_shallowDumpClientNode,
      'domain' =>   \&_shallowDumpDomainNode
    }
  );

  my $migrationDumpNode = _createMigrationDumpNode(
    1, # shallow dump has no content, configuration is dumped only
    $dumpServerSettings,
    @xmlNodes
  );

  unless (_serializeXmlNode($migrationDumpNode, "$workDir/$DUMP_FILE_NAME")) {
    Logging::error("Dump file '$DUMP_FILE_NAME' is not saved.");
    return 0;
  }

  return 1;
}

#
# END Interface methods
#

# A tree node is a reference to a hash with keys:
# - type - values are: 'admin', 'reseller', 'client', 'domain'
# - id: id of a object (reseller id, client id, domain id, undef for admin)
# - children - reference to a list of child nodes
#
# Root (admin) node is returned
#
# Tree structure is the following:
# --- admin
#   |--- reseller
#      |--- client
#         |--- domain
#      |--- domain
#   |--- client
#      |--- domain
#   |--- domain
#
# In the current implementation, admin node have reseller, client and domain child nodes
#
# So, the final result of this function looks like:
# {
#   'type' => 'admin',
#   'id' => undef, # admin id is undef in the current implementation
#   'children' => [
#     {
#       'type' => 'reseller',
#       'id' => 'some-reseller-id',
#       'children' => [
#         {
#           'type' => 'client',
#           'id' => 'some-client-id',
#           'children' => [
#             {
#               'type' => 'domain',
#               'id' => 'some-domain-id',
#               'children' => [] # domain node has no children in the current implementation
#             },
#             ... # another domain nodes
#           ]
#         },
#         ..., # another client nodes
#         {
#           'type' => 'domain',
#           'id' => 'some-domain-id',
#           'children' => [] # domain node has no children in the current implementation
#         },
#         ... # another domain nodes
#       ]
#     },
#     ..., # another reseller nodes
#     {
#       'type' => 'client',
#       'id' => 'some-client-id',
#       'children' => [
#         {
#           'type' => 'domain',
#           'id' => 'some-domain-id',
#           'children' => [] # domain node has no children in the current implementation
#         },
#         ... # another domain nodes
#       ]
#     },
#     ..., # another client nodes
#     {
#       'type' => 'domain',
#       'id' => 'some-domain-id',
#       'children' => [] # domain node has no children in the current implementation
#     }
#   ]
# }
sub _createFullTree {
  my ($getDomainsFunction) = @_;

  unless ($getDomainsFunction) {
    $getDomainsFunction = \&Agent::getDomains;
  }

  my @adminResellers =
      map { _createResellerSubtree($getDomainsFunction, $_) } Agent::getResellers();
  my @adminClients =
      map { _createClientSubtree($getDomainsFunction, $_) } Agent::getClients();
  my @adminDomains =
      map { _createDomainSubtree($_) } $getDomainsFunction->(undef);

  return {
    'type' => 'admin',
    'id' => undef,
    'children' => [ @adminResellers, @adminClients, @adminDomains ]
  };
}

# In the current implementation, reseller nodes have client and domain child nodes
sub _createResellerSubtree {
  my ($getDomainsFunction, $resellerId) = @_;

  my @resellerClients =
    map { _createClientSubtree($getDomainsFunction, $_) } Agent::getClients($resellerId);
  my @domainClients =
    map { _createDomainSubtree($_) } $getDomainsFunction->($resellerId);

  return {
    'type' => 'reseller',
    'id' => $resellerId,
    'children' => [ @resellerClients, @domainClients ]
  };
}

# In the current implementation, client nodes have domain child nodes
sub _createClientSubtree {
  my ($getDomainsFunction, $clientId) = @_;

  my @clientDomains =
    map { _createDomainSubtree($_) } $getDomainsFunction->($clientId);

  return {
    'type' => 'client',
    'id' => $clientId,
    'children' => [ @clientDomains ]
  };
}

# In the current implementation, domain nodes have no child nodes
sub _createDomainSubtree {
  my ($domainId) = @_;

  return {
    'type' => 'domain',
    'id' => $domainId,
    'children' => []
  }
}

# Walk tree recursively:
# - for every child node call _walkTree recursively, put result in a list
# - call function depending on a node type with arguments: node id, the list from the previous step
# - return function result
#
# @param $rootNode - node to start walking from
# @param $dispatchTable - reference to a hash with keys - node type, value - functions, that can process particular node type
# @param $selectedObjects - hashref that contains objects to be dumped
# @param @dumpOptions - list of CLI dump options; passed through to a node type function and recursive _walkTree calls
#
# Notes:
# - recursive call is done first (hidden in the node type handler parameters computation), then node 
#   type handler itself is called. So, kinda crutch was introduced for tree path tracking (BranchTracker).
# - branchTracker stores current branch _walkTree is in from the resulting XML/file layout 
#   point-of-view. It then later used in the Storage::ContentNameCreator to generate filesystem
#   paths for the content files. save/restore are needed for path saving during recursive calls.
#   Example: 
#     reseller 'res1', client 'web3' and domain 'web3.confixx.local' is translated to:
#     'resellers/res1/clients/web3/domains/web3.confixx.local' path component.
sub _walkTree {
  my ($rootNode, $dispatchTable, $selectedObjects, @dumpOptions) = @_;

  my $branchTracker = BranchTracker->instance();
  $branchTracker->save();
  
  my $entityId = $rootNode->{'id'};

  if( $rootNode->{'type'} eq 'reseller' ) {
    if( exists $selectedObjects->{ 'resellers' }->{ $entityId } ) {
      $branchTracker->push( 'resellers', $entityId );
    }
    Logging::trace( "Path branch for reseller $entityId is " . join('/', $branchTracker->getPathAsArray()) );
  } elsif( $rootNode->{'type'} eq 'client' ) {
    if( exists $selectedObjects->{ 'clients' }->{ $entityId } ) {
      $branchTracker->push( 'clients', $entityId );
    }
    Logging::trace( "Path branch for client $entityId is " . join('/', $branchTracker->getPathAsArray()) );
  } # no 'domains' type because it is supposed to be absent in the tree

  my $nodeFunction = $dispatchTable->{$rootNode->{'type'}};
  my $rv = $nodeFunction->(
    $rootNode->{'id'},
    [ map { _walkTree($_, $dispatchTable, $selectedObjects, @dumpOptions) } @{$rootNode->{'children'}} ],
    $selectedObjects, 
    @dumpOptions
  );

  $branchTracker->restore();

  return $rv;
}

#
# BEGIN Shallow dump functions ("-da -nc" command line arguments)
#

sub _shallowDumpAdminNode {
  my (undef, $childResult) = @_;

  my %subObjects = _joinArrayHashes(@$childResult);

  return XmlNode->new('admin',
    'attributes' => {'guid' => ConfixxGuidGenerator::getAdminGuid()},
    'children' => [
      _makeSubObjectsXmlNodes(%subObjects),
      Agent::getAdminPreferences()
    ]
  );
}

sub _shallowDumpResellerNode {
  my ($resellerId, $childResults) = @_;

  my $resellerContact = "";
  $resellerContact = Agent::getResellerContactString($resellerId) if Agent->can( 'getResellerContactString' );

  my $resellerNode = XmlNode->new('reseller',
    'attributes' => {
      'name' => $resellerId,
      'guid' => ConfixxGuidGenerator::getResellerGuid($resellerId),
      'contact' => $resellerContact,
    },
    'children' => [
      XmlNode->new('preferences'),
      _makeDummyPropertiesNode(),
      CommonXmlNodes::ipPool(Agent::getResellerIPPool($resellerId)),
      _makeSubObjectsXmlNodes(_joinArrayHashes(@$childResults))
    ]
  );

  return {'resellers' => [$resellerNode]};
}

sub _shallowDumpClientNode {
  my ($clientId, $childResults) = @_;

  my $clientContact = '';
  $clientContact = Agent::getClientContactString($clientId) if Agent->can( 'getClientContactString' );

  my $resellerName = Dumper::getResellerForClient($clientId);
  my $clientNode = XmlNode->new('client',
    'attributes' => {
      'name' => $clientId,
      'guid' => ConfixxGuidGenerator::getClientGuid($clientId),
      'contact' => $clientContact,
      'vendor-login' => $resellerName,
    },
    'children' => [
      XmlNode->new('preferences'),
      _makeDummyPropertiesNode(),
      CommonXmlNodes::ipPool(Agent::getClientIPPool($clientId)),
      _makeSubObjectsXmlNodes(_joinArrayHashes(@$childResults))
    ]
  );

  return {'clients' => [$clientNode]};
}

sub _shallowDumpDomainNode {
  my ($domainId) = @_;

  my $domainNode = XmlNode->new('domain',
    'attributes' => {
      'guid' => ConfixxGuidGenerator::getDomainGuid($domainId),
      'auxiliary' => 'true',
    },
    'children' => [
      XmlNode->new('preferences'),
      Agent::_getDomainCapabilitiesNode($domainId),
    ]
  );

  # domain names are always in UTF-8 for IDN domains to work fine
  $domainNode->setAttribute('name', $domainId, 'UTF-8');
  $domainNode->setAttribute('name', Agent::getClientSystemDomainName($domainId), 'UTF-8');

  return {'domains' => [$domainNode]};
}

sub _makeDummyPropertiesNode {
  return XmlNode->new('properties',
    'children' => [
      CommonXmlNodes::emptyPassword(),
      CommonXmlNodes::status(1)
    ]
  );
}

#
# END Shallow dump functions ("-da -nc" command line arguments)
#

#
# BEGIN Selection dump functions
#

sub _selectionDumpComposeAdminNode {
  my (undef, $childResults, $selectedObjects, $dumpOptions) = @_;
 
  my $adminPreferences = defined ( $dumpOptions->{'dumpServerSettings'} ) ? Agent::getAdminPreferences() : undef;
  return XmlNode->new('admin',
    'attributes' => {'guid' => ConfixxGuidGenerator::getAdminGuid()},
    'children' => [
      _makeSubObjectsXmlNodes(_joinArrayHashes(@$childResults)),
      $adminPreferences 
    ]
  );
}

sub _selectionDumpProcessReseller {
  my ($resellerId, $childResults, $selectedObjects, $dumpOptions) = @_;

  Logging::debug("Dumping reseller '$resellerId'");
  $dumpStatus->startClient( $resellerId );

  my %subObjects = _joinArrayHashes(@$childResults);

  my $returnValue = \%subObjects;

  if ($selectedObjects->{'resellers'}->{$resellerId}) {
    my $resellerXmlNode = Agent::getReseller($resellerId);
    $resellerXmlNode->addChildren(_makeSubObjectsXmlNodes(%subObjects));

    $returnValue = {'resellers' => [$resellerXmlNode]};
  }
  $dumpStatus->endClient( $resellerId );
  return $returnValue;
}

sub _selectionDumpProcessClient {
  my ($clientId, $childResults, $selectedObjects, $dumpOptions) = @_;

  Logging::debug("Dumping client '$clientId'");
  $dumpStatus->startClient( $clientId );

  my %subObjects = _joinArrayHashes(@$childResults);

  my $returnValue = \%subObjects;

  if ($selectedObjects->{'clients'}->{$clientId}) {
    my $clientXmlNode = Agent::getClient($clientId, $dumpOptions);
    $clientXmlNode->addChildren(_makeSubObjectsXmlNodes(%subObjects));

    $returnValue = {'clients' => [ $clientXmlNode ]}
  }
  $dumpStatus->endClient( $clientId );
  return $returnValue;
}

# Join multiple hashes (of arrays) into single hash (of arrays).
#
# example:
#   input: (
#     { 'resellers' => [1, 2], 'clients' => [3, 4], 'domains' => [5, 6] }
#     { 'resellers' => [7, 8], 'clients' => [9, 10, 3], 'domains' => [11] }
#   )
#   output:
#     { 'resellers' => [1, 2, 7, 8], 'clients' => [3, 4, 9, 10, 3], 'domains' => [5, 6, 11] }
sub _joinArrayHashes {
  my (@hashes) = @_;

  my %result;

  for my $hash (@hashes) {
    while (my ($key, $value) = each %$hash) {
      push @{$result{$key}}, @$value;
    }
  }

  return %result;
}

#
# END Selection dump functions
#

#
# BEGIN Get all objects ids functions
#

sub _getAllResellers {
  return _getObjectIdsOfType('reseller');
}

sub _getAllClients {
  return _getObjectIdsOfType('client');
}

sub _getAllDomains {
  return _getObjectIdsOfType('domain');
}

sub _getObjectIdsOfType {
  my ($type) = @_;

  return
    map { $_->{'id'} } # get id
      grep { $_->{'type'} eq $type } # filter nodes of specified type
        _getTreeNodesList(_createFullTree()); # make list of all tree nodes
}

sub _getTreeNodesList {
  my ($rootTreeNode) = @_;

  my @treeNodes;
  my @stack = ($rootTreeNode);

  while (@stack) {
    my $node = pop @stack;
    push @treeNodes, $node;
    push @stack, @{$node->{'children'}};
  }

  return @treeNodes;
}

#
# END Get all objects ids functions
#

sub _createMigrationDumpNode {
  my ($configOnly, $dumpServerSettings, @dumpDataNodes) = @_;

  return XmlNode->new('migration-dump',
    'attributes' => {
      'agent-name' => Agent::getAgentName(),
      'dump-version', Agent::getAgentVersion(),
      'content-included', $configOnly ? 'false' : 'true'
    },
    'children' => [
      _createDumpInfoNode(),
      @dumpDataNodes
    ]
  );
}

sub _createDumpInfoNode {
  my $contentTransportDescriptionNode = Agent::getContentTransportDescription();

  return XmlNode->new('dump-info',
    'children' => [
      XmlNode->new('cp-description',
        'attributes' => {'type', Agent::getAgentName()}
      ),
      ref($contentTransportDescriptionNode) eq 'XmlNode'?
        XmlNode->new('content-transport', 'children' => [ $contentTransportDescriptionNode ]) : undef
    ]
  );
}

sub _makeDomainNode {
  my ($clientId, $domainIds, $dumpOptions) = @_;

  # The dump returned in 'recursive' mode doesn't have content and is always 'full'.
  my $recursive = 'recursive';

  my %contentOptions;
  $contentOptions{'only-mail'} = 1 if defined ( $dumpOptions->{'onlyMail'} );
  $contentOptions{'only-hosting'} = 1 if defined ( $dumpOptions->{'onlyHosting'} );
  $contentOptions{'dummy-content'} = 1 if defined ( $dumpOptions->{'dummyContent'} );

  my $domainNode = Agent::getDomainGroup( $clientId, $domainIds, $recursive, $dumpOptions->{'configOnly'} ? undef : 'content', $dumpOptions->{'configOnly'} ? undef : $dumpOptions->{'gzip'}, \%contentOptions);

  unless ( ref($domainNode) =~ /XmlNode/ ) {return undef};
  unless ($domainNode->getName() eq 'domain' ) {return undef};

  ####
  # Process 'onlyMail' or 'onlyHosting' cases.
  #
  if ( $dumpOptions->{'onlyMail'} ) {
    my $onlyMailDomain = $domainNode->copy('no_children');

    $onlyMailDomain->addChild( XmlNode->new ( 'preferences' ) );

    my $onlyMailPropertiesNode = XmlNode->new( 'properties' );
    my $propertiesNode = $domainNode->getChild( 'properties' );
    my $ipNode = $propertiesNode->getChild( 'ip' );
    $onlyMailPropertiesNode->addChild( $ipNode ) if defined ( $ipNode );
    $onlyMailPropertiesNode->addChild( $propertiesNode->getChild( 'status' ) );

    $onlyMailDomain->addChild( $onlyMailPropertiesNode );

    my $mailsystemNode = $domainNode->getChild( 'mailsystem' );
    $onlyMailDomain->addChild( $mailsystemNode ) if defined ( $mailsystemNode );

    my $maillistsNode = $domainNode->getChild( 'maillists' );
    $onlyMailDomain->addChild( $maillistsNode ) if defined ( $maillistsNode );

    return $onlyMailDomain;
  }
  elsif ( $dumpOptions->{'onlyHosting'} ) {
    my $onlyHostingDomain = $domainNode->copy('no_children');

    $onlyHostingDomain->addChild( $domainNode->getChild( 'preferences' ) );

    $onlyHostingDomain->addChild( $domainNode->getChild( 'properties' ) );

    my $limitsAndPermissionsNode = $domainNode->getChild( 'limits-and-permissions' );
    $onlyHostingDomain->addChild( $limitsAndPermissionsNode ) if defined ( $limitsAndPermissionsNode );

    my $databasesNode = $domainNode->getChild( 'databases' );
    $onlyHostingDomain->addChild( $databasesNode ) if defined ( $databasesNode );

    my @odbcDsnNodes = $domainNode->getChildren( 'odbc-dsn' );
    if ( @odbcDsnNodes ) {
      foreach my $odbcDsnNode ( @odbcDsnNodes ) {
        $onlyHostingDomain->addChild( $odbcDsnNode );
      }
    }

    my @coldfusionDsnNodes = $domainNode->getChildren( 'coldfusion-dsn' );
    if ( @coldfusionDsnNodes ) {
      foreach my $coldfusionDsnNode ( @coldfusionDsnNodes ) {
        $onlyHostingDomain->addChild( $coldfusionDsnNode );
      }
    }

    my $trafficNode = $domainNode->getChild( 'traffic' );
    $onlyHostingDomain->addChild( $trafficNode ) if defined ( $trafficNode );

    my $certificatesNode = $domainNode->getChild( 'certificates' );
    $onlyHostingDomain->addChild( $certificatesNode ) if defined ( $certificatesNode );

    my $tomcatNode = $domainNode->getChild( 'tomcat' );
    $onlyHostingDomain->addChild( $tomcatNode ) if defined ( $tomcatNode );

    my $domainuserNode = $domainNode->getChild( 'domainuser' );
    $onlyHostingDomain->addChild( $domainuserNode ) if defined ( $domainuserNode );

    my $phostingNode = $domainNode->getChild( 'phosting' );
    if ( defined ( $phostingNode ) ) {
      $onlyHostingDomain->addChild( $phostingNode );
    }
    else {
      my $shostingNode = $domainNode->getChild( 'shosting' );
      if ( defined ( $shostingNode ) ) {
        $onlyHostingDomain->addChild( $shostingNode );
      }
      else {
        my $fhostingNode = $domainNode->getChild( 'fhosting' );
        if ( defined ( $fhostingNode ) ) {
          $onlyHostingDomain->addChild( $fhostingNode );
        }
      }
    }

    return $onlyHostingDomain;
  }
  # END: Process 'onlyMail' or 'onlyHosting' cases.
  ####

	return $domainNode;
}

#
# Store $xmlNode to $filename
#
sub _serializeXmlNode {
  my ($xmlNode, $filename) = @_;

  my $oldUmask = umask;
  umask 0027;
  my $openRv = open DUMP, ">$filename";
  umask $oldUmask;
  unless ( $openRv ) {
    Logging::error("Unable open file '". $filename . "' for write: $!");
    return;
  }
  binmode DUMP;
  $xmlNode->serialize(\*DUMP);
  close DUMP;
  return 1;
}
#
# Returns an XmlNode with content list.
#
# TODO probably this code should be replaced with functions from FileStorage.pm as it seems that they do the same, and they are used in PleskX agent to generate content list
sub _getContentList {
  my $dumpXmlNode = shift;
  unless ( ref($dumpXmlNode) =~ /XmlNode/ ) {return undef};

  my $contentListNode = XmlNode->new( 'contentlist' );
  $contentListNode->setAttribute( 'size', '0');

  _lookup4ContentNode($dumpXmlNode, $contentListNode);

  # add custom logging files into contentlist
  foreach my $logFile ( CustomLogging::closeAndGetLogsList() ) {
    _addFileToContentListNode($logFile, -s "$logFile", $contentListNode);
  }

  $contentListNode->addChild(XmlNode->new('file',
    'attributes' => {'size' => 0},
    'content' => $MIGRATION_RESULT_FILE_NAME
  ));

  return $contentListNode;
}

sub _lookup4ContentNode {
  my ($node, $contentListNode) = @_;

  my @childNodes = $node->getChildren( undef );
  if ( @childNodes ) {
    foreach my $childNode ( @childNodes ) {
      if ( $childNode->getName() eq 'content' ) {
        _processContentNode($childNode, $contentListNode);
      }
      else {
        _lookup4ContentNode($childNode, $contentListNode);
      }
    }
  }
}

sub _processContentNode {
  my ($node, $contentListNode) = @_;

  my @cidNodes = $node->getChildren( 'cid' );
  foreach my $cidNode ( @cidNodes ) {
    my $path = $cidNode->getAttribute( 'path' );
    $path = '' unless ( defined ( $path ) );
    $path .= '/' if $path and substr( $path, -1, 1 ) ne '/';
    my @contentFileNodes = $cidNode->getChildren( 'content-file' );
    foreach my $contentFileNode ( @contentFileNodes ) {
      _addFileToContentListNode($path . $contentFileNode->getText(), $contentFileNode->getAttribute('size'), $contentListNode);
    }
  }
}

sub _addFileToContentListNode {
  my ($filename, $size, $contentListNode) = @_;

  my $fileNode = XmlNode->new('file');
  $fileNode->setAttribute('size', $size || '0');
  $fileNode->setText($filename);
  $contentListNode->addChild($fileNode);

  _addSizeToContentListNode($size, $contentListNode);
}

sub _addSizeToContentListNode {
  my ($size, $contentListNode) = @_;

  unless(defined($size)) {
    return;
  }

  my $contentListSize = $contentListNode->getAttribute( 'size' ) || '0';
  my $contentListNewSize = int($contentListSize) + int($size);
  $contentListNode->setAttribute( 'size', $contentListNewSize );
}

sub _hashSet {
  my (@list) = @_;

  return map { $_ => 1 } @list;
}

sub _makeSubObjectsXmlNodes {
  my (%subObjects) = @_;

  my @result;

  for my $objectType ('resellers', 'clients', 'domains') {
    if (defined($subObjects{$objectType}) && @{$subObjects{$objectType}}) {
      push @result, XmlNode->new($objectType, 'children' => $subObjects{$objectType});
    }
  }

  return @result;
}

1;
