
#/*
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you 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.
#*/


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#
# Insert or update (iou) the license in a given file.  The license is
# assumed to be located in a comment block that contains a particular
# license string ($licenseString) somewhere inside it, and that the 
# comment contains nothing but that license.  The entire comment including
# that line will be removed, and a new comment consisting solely of the
# new license will be inserted.
#
# The license is read from "iou_license.txt", either in the local
# directory where this script is invoked or from the dirctory where the
# script itself lives (should be an option someday).  This is the text
# that will go *inside* the comment.  It does not contain any
# comment-related characters, so that the comment could be added to
# files with different comment styles.
#
# The license in "iou_license.txt" can contain an optional license
# version line that assigns a version string to that specific license
# content (eg, LicenseVersion v1.0 ).  When the text changes, this version
# string should change.  If the script encounters a file whose
# license has the same version (if versioning was used) as the current 
# one, the file will remain unchanged.
#
# Comments are understood on a per-file-type basis.
#
# Without arguments, this script will process all files within the 
# current directory and its sub-directories.  Alternatively, one or
# more files and/or directories can be specified as arguments.
#
#   Usage: iou_license.pl [item ...]
#
#          item: One or more files and/or directories to be processed
#
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~


#
# The following lines are designed to run perl on this script, no
# matter which shell the script is accidentally run under.  The point
# here is this: The normal way to invoke perl on a script is to put
# a line starting with #! at the begging of the script that invokes
# perl, as in "#!/usr/bin/perl".  The problem with this is that the
# script then hard codes the location of perl into itself, and when
# that changes the script breaks (and rather mysteriously at that).
# So this script purposefully specifies *no* interpreter at the top,
# and then adapts to whatever shell is running to get to perl.  (I
# tried putting #!/bin/sh at the top to guarantee the shell, but perl
# is too clever by half -- when the shell script executes perl, perl
# looks at the #! line and says "this is a shell script" and invokes
# the shell.)
#
# Yes this is ugly, but it is an Officially Certified Hack(TM), so
# no purist, anti-hacker, ivory tower kvetching.
#

eval '(exit $?0)' && eval 'exec perl -S $0 ${1+"$@"}'
    & eval 'exec perl -S $0 $argv:q'
	    if 0;

$scriptDir = $0;
$scriptDir =~ s,/[^/]+$,,;
push( @INC, $scriptDir );

$Verbose = 1;				# someday make this turn-off-able

$LicenseTextFileName = "iou_license.txt";
$LicenseTextFile       = "$scriptDir/$LicenseTextFileName"; # make this an option someday

@args = ( $#ARGV < 0 ? '.' : @ARGV );	# by default, use all source

#
# Set licenseString to a unique identifying string within the license text.
# This string will be used to determine whether we've found the license comment.
# It would be more desirable and less maintenance if this string were changed
# to a unique tag that could remain constant (eg, license_comment_start).  Till
# then, however, we'll have to live with what we have.
#
# It may also be useful to allow this string to be set on the command-line
# since source files can contain a wide variety of licenses.  For example,
# let's say we're importing a set of sources from The Foobar Software Depot,
# and those sources contain the "Common Foobar License" (CFL), we could use the
# a command line option to replace the CFL by executing a command like the
# following to override the default licenseString definition:
#
#	iou_license.pl -s "Common Foobar License"
#

$licenseString = "Licensed to the Apache Software Foundation";

# The ':'-separated pre, mid, and end comment strings for each file type

# Optional fourth and fifth elements are regular expressions for pre and end
# (otherwise a simple substring match is done).  If the fourth and/or fifth
# elements exist, they will override the simple pre and end elements.

$java  = '/*: *: */';
$shell = '#/*:#:#*/';
$bat   = '@rem /*:@rem:@rem */';
$html  = '<!--: !: !-->:\<\!(\n|\s|\-\-):.*(\-\-\s*)?\>';

$mapping{'.bat'}		= $bat;

$mapping{'.c'}			= $java;
$mapping{'.h'}			= $java;
$mapping{'.java'}		= $java;
$mapping{'.policy'}		= $java;

$mapping{'.html'}		= $html;
$mapping{'.jsp'}		= $html;
$mapping{'.jspfragment'}	= $html;
$mapping{'.xml'}		= $html;

$mapping{'.nmk'}		= $shell;
$mapping{'.jmk'}		= $shell;
$mapping{'.sh'}			= $shell;
$mapping{'.csh'}		= $shell;
$mapping{'.ksh'}		= $shell;
$mapping{'.pl'}			= $shell;
$mapping{'.properties'}		= $shell;
$mapping{'makefile'}		= $shell;
$mapping{'Makefile'}		= $shell;
$mapping{'GNUmakefile'}		= $shell;

# technically all you need is "<!--" and "-->" but we use the !s to draw
# a cutesy box.

&createGetPats();

#
# Process the arguments.  If the arg is a directory, recurse looking for
# known file types
#

foreach $arg ( @args )
{
    if ( -T $arg )	# text file
    {	
	push( @files, $arg );
    }
    elsif ( -d $arg )	# directory -- run find
    {		
	push( @files, split(/\s+/, $files = `find $arg \\( $names \\) -print`) );
    }
    else
    {
	print STDERR "unknown file type for $arg: ignoring\n";
    }
}

&getCurrentLicense();			# read the current license

$error = 0;				# process each file
foreach $file ( @files )
{
    $error |= &process_file( $file );
}

exit( $error );


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# process_file:
#	Process a single file.  Figure out from its suffix what kind of
#	comment to use (currently only .java, .c, and .h are understood).
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

sub process_file
{
    local ( $file ) = @_;

    if ( -l $file )
    {
	print "link: $file: skipping\n" if ( $Verbose );
	return;
    }

    local ( $pre, $mid, $end, $prePat, $endPat ) = &getPats( $file );

    if ( $pre eq 'UNDEF' )
    {
	print "unknown file type: $file: skipping\n" if ( $Verbose );
	return;
    }

    local ( $found_license );			# did we find the license comment?
    local ( $comment_start );			# current comment starting position
    local ( $in_comment );			# boolean stating whether we're inside a comment block
    local ( $license_start, $license_end );	# license comment start/end positions
    local ( $license_version );			# license version in file

    open( FILE, $file ) || die( "$file: $!" );

    $prePat = &pat( $pre ) unless ( defined($prePat) && $prePat ne '' );
    $endPat = &pat( $end ) unless ( defined($endPat) && $endPat ne '' );



    while ( <FILE> )				# search for existing license
    {				
	if ( $. == 1 && /^[#%]!/ )
	{					# skip over initial #! or %!
	    $license_start    = tell( FILE );	# overwritten if comment is...
	    $license_end      = tell( FILE );	# overwritten if comment is...
	    next;				# ...present later in file
	}

	##
	## If the prePat is closed by the endPat on the same line, a license
	## is probably not there:
	##

	if ( /$prePat.*$endPat/ )
	{
	    next;
	}
	elsif ( /$prePat/ )
	{
	    $in_comment = 1;

	    $comment_start = tell( FILE ) - length;
	}
	elsif ( /.*$licenseString/ && $in_comment )
	{
		$found_license++;
		$license_start = $comment_start;
	}
	elsif ( /LicenseVersion ([^\s]+)/ )
	{
	    $license_version = $1;
	}
	elsif ( /$endPat/ )
	{
	    $in_comment = 0;

	    ##
	    ## When the file is a script with a special first line (eg, #!/bin/sh), license_end will
	    ## be set to some number of characters greater than zero (ie, the length of that first line)
	    ## and license_start will be equal to license_end.  Otherwise, license_end will have
	    ## been set to zero, so we need to account for this special case:
	    ##

	    if ( $found_license && (!$license_end || ($license_start==$license_end)) )
	    {
		$license_end = tell( FILE );
	    }
	}
    }
    $found_license = 0 if ( $found_license && (!$license_end || ($license_start==$license_end)) );

    if ( $Verbose )
    {
	print "$file: ";

	if ( !$found_license )
	{
	    print "adding";
	}
	else
	{
	    print "replacing license ";
	    print ( $license_version ? $license_version: "(unversioned)" );
	}
	print "\n";
    }

    local ( $tmp ) = "$file.cr";			# create tmp file with new stuff

    open( NEW, ">$tmp" ) || die( "$tmp: $!" );
    seek( FILE, 0, 0 );
    read( FILE, $buf, $license_start );		# copy pre-comment stuff

    print NEW $buf;
    print NEW "$pre\n";

    foreach $l ( @License )			# put in license
    {
	print NEW $mid, ' ', $l;
    }

    print NEW "$end\n";

    seek( FILE, $license_end, 0 );		# copy post-comment stuff

    while ( read(FILE, $buf,4096) )
    {
	print NEW $buf;
    }

    close( NEW );				# rename tmp to cur
    close( FILE );

    if ( !rename($tmp, $file) )
    {
	print "couldn't rename files for $file: $!";
	return 1;
    }

    return 0;
}


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# getCurrentLicense
#	Read current license and optional version.
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

sub getCurrentLicense
{
    open( LICENSE, "$LicenseTextFileName" ) || open( LICENSE, $LicenseTextFile )
	|| die( "$LicenseTextFile: $!" );

    @License = <LICENSE>;
    close( LICENSE );

    foreach $_ ( @License )
    {
	if ( /LicenseVersion ([^\s]+)/ )
	{
	    $CurrentLicenseVersion = $1;
	    last;
	}
    }

    print "License version $CurrentLicenseVersion\n" if ( $Verbose && $CurrentLicenseVersion );
}


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# createGetPats
#	Build up the maps from file name to pre, mid, and end comment
#	strings by creating the getPats method.  Also define $name
#	for the find comment to find such things (if necessary).
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

sub createGetPats
{
    push( @matchList, 'sub getPats {' );
    push( @matchList, '    local ($_) = @_;' );

    foreach $type ( keys(%mapping) )
    {
	$map = $mapping{$type};
	( $pre, $mid, $end ) = ( $map =~ /^([^:]*)\:([^:]*)\:(.*)$/ );

	# Optional args
	$prePat = $endPat = undef;

	if ( $end =~ /^([^:]*)\:([^:]*)\:(.*)$/ )
	{
	    $prePat = $2;
	    $endPat = $3;
	    $end = $1;
	}
	if ( $type =~ /^\./ )
	{
	    push( @findArgs, "-name '*$type'" );
	    $pat = "m,\\$type\$,";
	}
	else
	{
	    push( @findArgs, "-name '$type*'" );
	    $pat = "(m,^$type, || m,/$type,)";
	}
	push( @matchList, "    $pat && return ( '$pre', '$mid', '$end', " .
	    (defined($prePat) ? "'$prePat', " : "undef, ") .
	    (defined($endPat) ? "'$endPat');" : "undef);") );
    }

    $names = join( ' -o ', @findArgs );

    push( @matchList, "    return ('UNDEF', 'UNDEF', 'UNDEF', undef, undef);" );
    push( @matchList, "}" );
    $getPats = join( "\n", @matchList, '' );
    eval $getPats;
}


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# pat
#	Turn a simple string into something usable in a pattern by
#	escaping the special characters
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

sub pat
{
    local ( $_ ) = @_;

    s/(\W)/\\$1/g;
    return $_;
}

