releasing/cbrtools/perl/Cleaner.pm
changeset 602 3145852acc89
equal deleted inserted replaced
600:6d08f4a05d93 602:3145852acc89
       
     1 # Copyright (c) 2002-2009 Nokia Corporation and/or its subsidiary(-ies).
       
     2 # All rights reserved.
       
     3 # This component and the accompanying materials are made available
       
     4 # under the terms of the License "Eclipse Public License v1.0"
       
     5 # which accompanies this distribution, and is available
       
     6 # at the URL "http://www.eclipse.org/legal/epl-v10.html".
       
     7 # 
       
     8 # Initial Contributors:
       
     9 # Nokia Corporation - initial contribution.
       
    10 # 
       
    11 # Contributors:
       
    12 # 
       
    13 # Description:
       
    14 # 
       
    15 #
       
    16 
       
    17 use strict;
       
    18 use RelData;
       
    19 use File::Spec;
       
    20 
       
    21 package Cleaner;
       
    22 
       
    23 sub New {
       
    24   my $class = shift;
       
    25   my $iniData = shift;
       
    26   my $remote = shift;
       
    27   my $verbose = shift;
       
    28   my $reallyClean = shift;
       
    29   
       
    30   die "Cleaner didn't get an inidata" unless $iniData;
       
    31   die "Must tell Cleaner whether you want remote or local!!!" unless defined $remote;
       
    32   
       
    33   my $self = {
       
    34     iniData => $iniData,
       
    35     remote => $remote,
       
    36     verbose => $verbose,
       
    37     reallyClean => $reallyClean,
       
    38     force => 0,
       
    39     relsToClean => {},
       
    40     relsToKeep => {},
       
    41     envsToKeep => {},
       
    42     relsToKeepAfter => {},
       
    43     envsToKeepAfter => {},
       
    44     keepAfter => undef,
       
    45     cleanTo => undef,
       
    46     remoteSite => undef,
       
    47     cleaningSubroutine => undef,
       
    48     expunge_already_cleaned => undef
       
    49   };
       
    50 
       
    51   bless $self, (ref $class || $class);
       
    52 
       
    53   $self->{remoteSite} = $iniData->RemoteSite if ($self->{remote});
       
    54 
       
    55   return $self;
       
    56 }
       
    57 
       
    58 sub SetCleaningSubroutine {
       
    59   my $self = shift;
       
    60   my $cleaningsub = shift;
       
    61   $self->{cleaningSubroutine} = $cleaningsub;
       
    62 }
       
    63 
       
    64 sub SetFinishingSubroutine {
       
    65   my $self = shift;
       
    66   $self->{finishingSubroutine} = shift;
       
    67 }
       
    68 
       
    69 sub SetRevertingSubroutine {
       
    70   my $self = shift;
       
    71   $self->{revertingSubroutine} = shift;
       
    72 }
       
    73 
       
    74 sub ProcessDescriptionLine {
       
    75   my $self = shift;
       
    76   my $descriptionFile = shift;
       
    77   my $keyWord = shift;
       
    78   my @operand = @_;
       
    79 
       
    80   if ($keyWord =~ /^keep_env$/) {
       
    81     unless ($#operand == 1) {
       
    82       die "Error: Incorrect number of arguments to \'$keyWord\' keyword in \"$descriptionFile\"\nSyntax: keep_env <component> <version>\n";
       
    83     }
       
    84     my $comp = lc($operand[0]);
       
    85     my $ver = lc($operand[1]);
       
    86     if (exists $self->{envsToKeep}->{$comp}->{$ver}) {
       
    87       die "Error: Environment \"$comp $ver\" specified for keeping more than once\n";
       
    88     }
       
    89     $self->{envsToKeep}->{$comp}->{$ver} = 1;
       
    90   }
       
    91   elsif ($keyWord =~ /^keep_rel$/) {
       
    92     unless ($#operand == 1) {
       
    93       die "Error: Incorrect number of arguments to \'$keyWord\' keyword in \"$descriptionFile\"\nSyntax: keep_rel <component> <version>\n";
       
    94     }
       
    95     my $comp = lc($operand[0]);
       
    96     my $ver = lc($operand[1]);
       
    97     $self->{relsToKeep}->{$comp}->{$ver} = 1;
       
    98   }
       
    99   elsif ($keyWord eq "keep_recent_env") {
       
   100     unless ($#operand == 1) {
       
   101       die "Error: Incorrect number of arguments to \'$keyWord\' keyword in \"$descriptionFile\"\nSyntax: keep_recent_env <component> <num_days>\n";
       
   102     }
       
   103     my $comp = lc($operand[0]);
       
   104     
       
   105     my $time = $operand[1];
       
   106     
       
   107     if ($time !~ /^\d+$/) {
       
   108       die "Error: The <num_days> argument for the '$keyWord' keyword must be a positive number\n";
       
   109     }
       
   110     
       
   111     $time = time - ($time * 60 * 60 * 24);   
       
   112     
       
   113     if (exists $self->{envsToKeepAfter}->{$comp}) {
       
   114       die "Error: keep_recent_env called more than once on component \'$comp\' in \"$descriptionFile\"\n";
       
   115     }
       
   116     $self->{envsToKeepAfter}->{$comp} = $time;
       
   117   }
       
   118   elsif ($keyWord eq "keep_recent_rel") {
       
   119     if ($#operand == 0) {
       
   120       if (defined $self->{keepAfter}) {
       
   121         die "Error: \'$keyWord\' keyword used more than once with no component name in \"$descriptionFile\"\n";
       
   122       }
       
   123       else {
       
   124         my $keepAfter = $operand[0];
       
   125         
       
   126         if ($keepAfter !~ /^\d+$/) {
       
   127           die "Error: The <num_days> argument for the '$keyWord' keyword must be a positive number\n";
       
   128         }
       
   129 
       
   130         $self->{keepAfter} = time - ($keepAfter * 60 * 60 * 24);
       
   131       }
       
   132     }
       
   133     elsif ($#operand == 1) {
       
   134       my $comp = lc($operand[0]);
       
   135       my $time = $operand[1];
       
   136       
       
   137       if ($time !~ /^\d+$/) {
       
   138         die "Error: Error: The <num_days> argument for the '$keyWord' keyword must be a positive number\n";
       
   139       }
       
   140       
       
   141       $time = time - ($time * 60 * 60 * 24);
       
   142       if (exists $self->{relsToKeepAfter}->{$comp}) {
       
   143         die "Error: keep_recent_rel called more than once on component \'$comp\' in \"$descriptionFile\"\n";
       
   144       }
       
   145       $self->{relsToKeepAfter}->{$comp} = $time;
       
   146     }
       
   147     else {
       
   148       die "Error: Incorrect number of arguments to \'$keyWord\' keyword in \"$descriptionFile\"\nSyntax: keep_recent_rel [<component>] <num_days>\n";
       
   149     }
       
   150   } 
       
   151   elsif ($keyWord =~ /^keep_recent$/) {
       
   152     unless ($#operand == 0) {
       
   153       die "Error: Incorrect number of arguments to \'$keyWord\' keyword in \"$descriptionFile\"\nSyntax: keep_recent <num_days>\n";
       
   154     }
       
   155     if (defined $self->{keepAfter}) {
       
   156       die "Error: \'$keyWord\' keyword used more than once in \"$descriptionFile\"\n";
       
   157     }
       
   158     
       
   159     my $keepAfter = $operand[0];
       
   160     
       
   161     if ($keepAfter !~ /^\d+$/) {
       
   162       die "Error: The <num_days> argument for the '$keyWord' keyword must be a positive number\n";
       
   163     }
       
   164     
       
   165     $self->{keepAfter} = time - ($keepAfter * 60 * 60 * 24);  
       
   166     print "Warning: The 'keep_recent' keyword has been deprecated, as it\nresults in broken environments. You can use the 'keep_recent_rel' keyword\nwithout a component name instead if you really mean this, to get rid of this\nwarning.\n";
       
   167   } elsif ($keyWord =~ /^force$/) {
       
   168     if (@operand) {
       
   169       die "Error: Incorrect number of arguments to \'$keyWord\' keyword in \"$descriptionFile\"\nSyntax: force\n";
       
   170     }
       
   171     if ($self->{force}) {
       
   172       die "Error: \'$keyWord\' keyword used more than once in \"$descriptionFile\"\n";
       
   173     }
       
   174     $self->{force} = 1;
       
   175   }
       
   176   else {
       
   177     return 0;
       
   178     
       
   179   }
       
   180   return 1;
       
   181 }
       
   182 
       
   183 sub PrintEnvsToKeep {
       
   184   my $self = shift;
       
   185   print "Environments to keep:\n";
       
   186   $self->TablePrintHash($self->{envsToKeep});
       
   187 }
       
   188 
       
   189 # Reads {envsToKeep} and {envsToKeepAfter}, updates {envsToKeep}, and fills out {relsToKeep}.
       
   190 sub FindRelsToKeep {
       
   191   my $self = shift;
       
   192 
       
   193   # Convert envsToKeepAfter into a list of envsToKeep
       
   194   foreach my $keepEnv (keys %{$self->{envsToKeepAfter}}) {
       
   195     my $keepAfter = $self->{envsToKeepAfter}->{$keepEnv};
       
   196 
       
   197     foreach my $ver (keys %{$self->{archiveComponents}->{$keepEnv}}) {
       
   198       # Check reldata time
       
   199       my $timestamp;
       
   200       if ($self->{remote}) {
       
   201         my $file = $self->{iniData}->PathData->RemoteArchivePathForExistingComponent($keepEnv, $ver, $self->{iniData}->RemoteSite);
       
   202         die "Failed to find path for \"$keepEnv\" \"$ver\"\n" unless $file;
       
   203         $file .= "/$keepEnv$ver.zip";
       
   204         $timestamp = $self->{remoteSite}->FileModifiedTime($file);
       
   205         
       
   206       } elsif (-e File::Spec->catfile($self->GetPathForExistingComponent($keepEnv, $ver), 'reldata')) {
       
   207         my $relData = RelData->Open($self->{iniData}, $keepEnv, $ver, $self->{verbose});
       
   208         $timestamp = $relData->ReleaseTime();
       
   209       } else {
       
   210         next;
       
   211       }
       
   212 
       
   213       if ($timestamp >= $keepAfter) {
       
   214         $self->{envsToKeep}->{$keepEnv}->{$ver} = 1; # It's new; keep it
       
   215       }
       
   216     }
       
   217   }
       
   218 
       
   219   # Convert envsToKeep into a list of relsToKeep
       
   220   foreach my $thisComp (sort(keys %{$self->{envsToKeep}})) {
       
   221     foreach my $thisVer (sort(keys %{$self->{envsToKeep}->{$thisComp}})) {
       
   222       if ($self->{verbose}) { print "Reading release data from $thisComp $thisVer...\n"; }
       
   223    
       
   224       my $thisCompPath = $self->{iniData}->PathData->LocalArchivePathForExistingComponent($thisComp, $thisVer);
       
   225      
       
   226       if ($thisCompPath) {
       
   227         $thisCompPath = File::Spec->catfile($thisCompPath, 'reldata'); 
       
   228       } else {
       
   229         if ($self->{remote}) {
       
   230           die "Error: Unable to continue since cleanremote requires a corresponding version of '$thisComp $thisVer' in your local archive(s).  Please check that your CBR configuration file is in order and is pointing to the correct location for your local archive(s).  Failing this you will need to ensure you have a copy of '$thisComp $thisVer' in one of your configured local archives\n";      
       
   231         } else {
       
   232           die "Internal error:  Release not found in local archive when attempting to get environment for kept component\n";
       
   233         }
       
   234       }
       
   235       
       
   236       if (-e $thisCompPath) {  
       
   237         my $thisRelData = RelData->Open($self->{iniData}, $thisComp, $thisVer, $self->{verbose});
       
   238         my $thisRelEnv = $thisRelData->Environment();
       
   239    
       
   240         foreach my $compToKeep (keys %{$thisRelEnv}) {
       
   241           my $verToKeep = $thisRelEnv->{$compToKeep};
       
   242           $self->{relsToKeep}->{lc($compToKeep)}->{lc($verToKeep)} = 1;
       
   243           delete $self->{archiveComponents}->{$compToKeep}->{$verToKeep}; # saves time when finding components to remove
       
   244         }
       
   245       } elsif ($self->{remote}) {
       
   246         die "Error: Unable to continue because the environment for '$thisComp $thisVer' could not be identified (corrupt release; missing reldata file)\n";
       
   247       } else {
       
   248         print "Warning: Unable to identify the environment for '$thisComp $thisVer'. This may result in additional component releases being cleaned from the archive.  (Corrupt release; missing reldata file)\n";
       
   249       }
       
   250     }
       
   251   }
       
   252 }
       
   253 
       
   254 sub Clean {
       
   255   my $self = shift;
       
   256 
       
   257   # remoteSite may be defined, or it may not...
       
   258   # If not, then this will operate on the local archive  
       
   259   foreach my $archiveComponent (@{$self->{iniData}->PathData->ListComponents($self->{remoteSite})}) {
       
   260     map {$self->{archiveComponents}->{$archiveComponent}->{$_} = 1} $self->{iniData}->PathData->ListVersions($archiveComponent, $self->{remoteSite});
       
   261   }
       
   262 
       
   263   $self->FindRelsToKeep();
       
   264 
       
   265   if ($self->{verbose} > 1) {
       
   266     print "Releases to keep:\n";
       
   267     $self->TablePrintHash($self->{relsToKeep});
       
   268   }
       
   269 
       
   270   $self->FindRelsToClean();
       
   271   
       
   272   if (%{$self->{relsToClean}}) {
       
   273     print "About to clean the following releases:\n";
       
   274     $self->TablePrintHash($self->{relsToClean});
       
   275     if ($self->Query("Continue?")) {
       
   276       $self->CleanReleases();
       
   277     }
       
   278     else {
       
   279       print "Aborting...\n";
       
   280       exit;
       
   281     }
       
   282   }
       
   283   else {
       
   284     print "Nothing to clean\n";
       
   285   }
       
   286 }
       
   287 
       
   288 # Walks the archive, filling out %relsToClean with releases that are not present in %relsToKeep.
       
   289 sub FindRelsToClean {
       
   290   my $self = shift;
       
   291 
       
   292   select STDOUT; $|=1;
       
   293   
       
   294   foreach my $thisArchComp (keys %{$self->{archiveComponents}}) {
       
   295     foreach my $ver (keys %{$self->{archiveComponents}->{$thisArchComp}}) {
       
   296       $self->CheckComp($thisArchComp, $ver);
       
   297     }
       
   298   }
       
   299 }
       
   300 
       
   301 sub CheckComp {
       
   302   my $self = shift;
       
   303   my $comp = lc(shift);
       
   304   my $thisVer = shift;
       
   305 
       
   306   unless (exists $self->{relsToKeep}->{$comp}->{lc($thisVer)}) {
       
   307     my $timestamp;
       
   308     if ($self->{remote}) {
       
   309       my $file = $self->{iniData}->PathData->RemoteArchivePathForExistingComponent($comp, $thisVer, $self->{iniData}->RemoteSite);
       
   310       die "Failed to find path for \"$comp\" \"$thisVer\"\n" unless $file;
       
   311       $file .= "/$comp$thisVer.zip";
       
   312       $timestamp = $self->{remoteSite}->FileModifiedTime($file);
       
   313     } elsif (-e File::Spec->catfile($self->GetPathForExistingComponent($comp, $thisVer), 'reldata')) {
       
   314           my $relData = RelData->Open($self->{iniData}, $comp, $thisVer, $self->{verbose});
       
   315           $timestamp = $relData->ReleaseTime();
       
   316     } elsif (!$self->{reallyClean}) {
       
   317           print "Warning: $comp $thisVer is not a complete release in " . $self->GetPathForExistingComponent($comp, $thisVer) . '.' .
       
   318           "\nThe component may be in the process of being released into the archive or it may be corrupt." .
       
   319           "\nRe-run with the -r option to remove this release from the archive.\n";
       
   320           return;
       
   321     }
       
   322     else {
       
   323           $self->{relsToClean}->{$comp}->{lc($thisVer)} = $thisVer;
       
   324           return;
       
   325     }
       
   326          
       
   327     if ($self->{keepAfter} && $timestamp >= $self->{keepAfter}) {
       
   328       print "Not cleaning $comp $thisVer - too new\n";
       
   329       return;
       
   330     }
       
   331     if (exists($self->{relsToKeepAfter}->{$comp}) && $timestamp >= $self->{relsToKeepAfter}->{$comp}) {
       
   332       print "Not cleaning $comp $thisVer - too new\n";
       
   333       return;
       
   334     }
       
   335     $self->{relsToClean}->{$comp}->{lc($thisVer)} = $thisVer;
       
   336   }
       
   337 }
       
   338 
       
   339 sub TablePrintHash {
       
   340   my $self = shift;
       
   341   my $hash = shift;
       
   342   my @tableData;
       
   343   foreach my $thisComp (sort keys %{$hash}) {
       
   344     foreach my $thisVer (sort keys %{$hash->{$thisComp}}) {
       
   345       push (@tableData, [$thisComp, $thisVer]);
       
   346     }
       
   347   }
       
   348   $self->{iniData}->TableFormatter->PrintTable(\@tableData);
       
   349   print "\n";
       
   350 }
       
   351 
       
   352 sub CleanReleases {
       
   353   my $self = shift;
       
   354 
       
   355   my $cleaningsub = $self->{cleaningSubroutine};
       
   356   die "No execution sub provided" unless ref $cleaningsub;
       
   357 
       
   358   my $failed = 0;
       
   359   my $cleaned = {};
       
   360 
       
   361   print "Cleaning...\n";
       
   362 
       
   363   foreach my $thisComp (sort keys %{$self->{relsToClean}}) {
       
   364     foreach my $thisVer (sort values %{$self->{relsToClean}->{$thisComp}}) { # use values to get correct case
       
   365       my $path = $self->GetPathForExistingComponent($thisComp, $thisVer);
       
   366       if (!defined($path)) {
       
   367         print "Unable to get path for $thisComp $thisVer: possible disconnection of FTP site?\n";
       
   368         $failed = 1;
       
   369         last;
       
   370       }
       
   371       elsif (&$cleaningsub($thisComp, $thisVer, $path)) {
       
   372         # Cleaning worked
       
   373         $cleaned->{$thisComp}->{lc($thisVer)} = [$thisVer, $path];
       
   374       }
       
   375       else {
       
   376         print "Unable to delete $thisComp $thisVer from $path\n";
       
   377         $failed = 1;
       
   378         last;
       
   379       }
       
   380     }
       
   381     if ($failed) {
       
   382       last;
       
   383     }
       
   384   }
       
   385 
       
   386   if ($failed) {
       
   387     my $revertsub = $self->{revertingSubroutine};
       
   388     if (ref $revertsub) {
       
   389       # Attempt to roll back
       
   390       print "Warning: Cleaning failed. Rolling back...\n";
       
   391       $failed = 0;
       
   392       foreach my $undoComp (sort keys %$cleaned) {
       
   393 	my @vers = map( $_->[0], values %{$cleaned->{$undoComp}} );
       
   394         foreach my $undoVer (sort @vers) {
       
   395           my $path = $cleaned->{$undoComp}->{lc($undoVer)}->[1];
       
   396           if (!&$revertsub($undoComp, $undoVer, $path)) {
       
   397             $failed = 1;
       
   398 	  }
       
   399 	}
       
   400       }
       
   401       if ($failed) {
       
   402         die "Warning: Cleaning failed and rollback also failed - the archive may have been left in an indeterminate state\n";
       
   403       }
       
   404     }
       
   405     else {
       
   406       # No rollback routine
       
   407       die "Warning: Cleaning failed - the archive may have been left in an indeterminate state\n";
       
   408     }
       
   409   }
       
   410   else {
       
   411     my $finishingsub = $self->{finishingSubroutine};
       
   412     if (ref $finishingsub) {
       
   413       # Finish the job
       
   414       foreach my $thisComp (sort keys %{$cleaned}) {
       
   415 	my @vers = map( $_->[0], values %{$cleaned->{$thisComp}} );
       
   416         foreach my $thisVer (sort @vers) {
       
   417           my $path = $cleaned->{$thisComp}->{lc($thisVer)}->[1];
       
   418           if (!&$finishingsub($thisComp, $thisVer, $path)) {
       
   419             print "Warning: Failed to complete cleaning of $thisComp at version $thisVer\n";
       
   420             $failed = 1;
       
   421           }
       
   422         }
       
   423       }
       
   424     }
       
   425     if (!$failed) {
       
   426       print "Cleaning complete.\n";
       
   427     }
       
   428   }
       
   429 }
       
   430 
       
   431 sub GetPathForExistingComponent {
       
   432   my $self = shift;
       
   433   my $thisComp = shift;
       
   434   my $thisVer = shift;
       
   435   my $path;
       
   436   if ($self->{remote}) {
       
   437     $path = $self->{iniData}->PathData->RemoteArchivePathForExistingComponent($thisComp, $thisVer, $self->{remoteSite});
       
   438   } else {
       
   439     $path = $self->{iniData}->PathData->LocalArchivePathForExistingComponent($thisComp, $thisVer);
       
   440   }
       
   441   return $path;
       
   442 }
       
   443 
       
   444 sub Query {
       
   445   my $self = shift;
       
   446   my $msg = shift;
       
   447 
       
   448   if ($self->{force}) {
       
   449     print "Skipping question \"$msg\" because of \"force\" keyword - assuming \"yes\"\n" if ($self->{verbose});
       
   450     return 1;
       
   451   }
       
   452 
       
   453   print "$msg [yes/no] ";
       
   454   my $response = <STDIN>;
       
   455   chomp $response;
       
   456   return ($response =~ m/^y/i);
       
   457 }
       
   458 
       
   459 1;
       
   460 
       
   461 __END__
       
   462 
       
   463 =head1 NAME
       
   464 
       
   465 Cleaner.pm - A module to clean an archive
       
   466 
       
   467 =head1 DESCRIPTION
       
   468 
       
   469 A module to clean an archive. Supposed to implement the common bits between C<cleanlocalarch> and C<cleanremote>, but the first of those commands has been temporarily suspended. The basic plan is: let it process the lines of your cleaning description file, then give it a subroutine to operate on the releases that should be cleaned. It will do the intervening stages of working out what releases should be kept, and which should be clean.
       
   470 
       
   471 =head1 INTERFACE
       
   472 
       
   473 =head2 New
       
   474 
       
   475 Pass it an IniData object, and a 0 or 1 to indicate whether it should act locally or remotely. If it's acting remotely, it will get a RemoteSite object from the IniData object.
       
   476 
       
   477 =head2 SetCleaningSubroutine
       
   478 
       
   479 Pass in a reference to a subroutine to actually do the first phase of cleaning. The subroutine will be passed the component name, the version number and the path. If this phase passes, the optional finishing routine will be called next. If it fails at any point, the reverting routine (if defined) will be called on each component which was 'cleaned'.
       
   480 
       
   481 =head2 SetFinishingSubroutine
       
   482 
       
   483 Pass in a reference to a 'finishing' subroutine to complete the cleaning (see L<SetCleaningSubroutine|setcleaningsubroutine>). If this routine has not been called then no finishing routine will be set up, and the clean will be said to have completed once the first phase is done. The finishing subroutine will be passed the component name, the version number and the path.
       
   484 
       
   485 =head2 SetRevertingSubroutine
       
   486 
       
   487 Pass in a reference to a 'reverting' subroutine to undo any 'cleaned' components (see L<SetCleaningSubroutine|setcleaningsubroutine>). If this routine has not been called then the cleaner will not attempt to revert changes if cleaning fails. The reverting subroutine will be passed the component name, the version number and the (original) path.
       
   488 
       
   489 =head2 ProcessDescriptionLine
       
   490 
       
   491 This should be passed the name of the description file (for error messages only), then a keyword, then an array of operands. It will interpret lines keep_rel, keep_env, force, and keep_recent. If it understands a line it returns 1; otherwise it returns 0.
       
   492 
       
   493 =head2 PrintEnvsToKeep
       
   494 
       
   495 This just prints a list of the environments it is going to keep.
       
   496 
       
   497 =head2 Clean
       
   498 
       
   499 This actually does the cleaning. It first finds the releases to keep, then finds the releases to clean, then runs the cleaning subroutine for each one.
       
   500 
       
   501 =head1 KNOWN BUGS
       
   502 
       
   503 None.
       
   504 
       
   505 =head1 COPYRIGHT
       
   506 
       
   507  Copyright (c) 2000-2009 Nokia Corporation and/or its subsidiary(-ies).
       
   508  All rights reserved.
       
   509  This component and the accompanying materials are made available
       
   510  under the terms of the License "Eclipse Public License v1.0"
       
   511  which accompanies this distribution, and is available
       
   512  at the URL "http://www.eclipse.org/legal/epl-v10.html".
       
   513  
       
   514  Initial Contributors:
       
   515  Nokia Corporation - initial contribution.
       
   516  
       
   517  Contributors:
       
   518  
       
   519  Description:
       
   520  
       
   521 
       
   522 =cut