blob: e1181ef125edb1aed5e9e7541b93833c05dc3d61 [file] [log] [blame]
#!/usr/bin/ruby -w
# Executable Ruby script to walk one or more directories worth of
# source files and add or update the copyright comment block on each file.
#
# File types are identified by extension, different file types
# vary in the format of the copyright comment block and its exact position.
#
# The template for each block will contain the string {YEAR}. This is replaced
# by the current year. However, if a current comment block exists,
# then the file is not touched (ASF rules is that the copyright year should
# only be updated when an actual change occurs to the file).
#
# The {ORG} placeholder in the template is replaced with the organization,
# which defaults to "The Apache Software Foundation", but can be overridden with the
# -o command line argument.
require 'find'
# Directory containing this script, used to locate templates (which are stored
# relative to the script itself.
SCRIPT_DIR = File.split(__FILE__)[0]
YEAR = Time.now.year.to_s
$ORG = "The Apache Software Foundation"
def read_template(file)
result = []
File.open(SCRIPT_DIR + "/" + file) do |file|
file.each { |line| result << line }
end
return result
end
# Writes out the content (array of strings) to the file.
# Actually, writes to a temporary file, then deletes the original
# file and renames the new file to it.
def write_file(path, content)
puts "Writing #{path} ..."
temp = path + "~"
File.open(temp, "w") do |file|
content.each { |line| file << line }
end
File.delete(path)
File.rename(temp, path)
end
# Scans the content (which should be the complete file)
# for the copyright year. Returns the year, which
# may be as single year ("2004") or a sequence of
# years ("2004, 2005, 2007"). Returns YEAR if
# no copyright year was found in the content.
def scan_for_year(content, comment_prefix)
content.each do |line|
if ! line.strip.empty? then
return YEAR if line[0, comment_prefix.length] != comment_prefix
if line =~ /copyright ((\d+)(\s*,\s*\d+)*)/i
then
return $1
end
end
end
# Degenerate case -- a file that contains just comments? Shouldn't happen
# but just in case.
return YEAR
end
# Synthesizes a copyright comment block by locating the {YEAR} token
# and substituting the year paremeter, and the {ORG} token with $ORG
def synthesize_copyright(template, year)
template.collect { |line| line.sub(/\{YEAR\}/, year).sub(/\{ORG\}/, $ORG) }
end
class Filter
def initialize(comment, template_file)
@comment = comment
@template = read_template(template_file)
end
def update(path)
content = nil
dirty = false
File.open(path) { |file| content = file.readlines }
year = scan_for_year(content, @comment)
copyright_comment = synthesize_copyright(@template, year)
0.upto(@template.length() - 1) do |line|
dirty ||= content[line] != copyright_comment[line]
end
# TODO: What if the new comment is *shorter* than the old comment?
# Need to find and trim those line.
return false if !dirty
# Strip out all leading blank lines and comments
while ! content.empty?
line = content[0]
if line.strip.empty? || line[0, @comment.length] == @comment
content.delete_at(0)
else
break
end
end
# content[0] should now be the package statement (or, if in the default package,
# an import, class, interface, etc.
content.insert(0, *copyright_comment)
# Write the new content to the file
write_file(path, content)
return true
end
end
# Filter for Java files. The copyright comment is placed
# before the first statement or directive (typically, before the package
# directive)
class JavaFilter < Filter
def initialize
super("//", "copyright-java.txt")
end
end
class PropertiesFilter < Filter
def initialize
super("#", "copyright-properties.txt")
end
end
# Filter used for any XML file. The copyright is placed after the <?xml ...?> line, and before
# anything else.
class XMLFilter
def initialize
@template = read_template("copyright-xml.txt")
end
# Returns true if the line looks like an XML "<!DOCTYPE ..",
# or element "<foo ..." This will not match an XML comment
# or the "<?xml ..." declaration.
def document_start?(line)
return line == nil || line.match(/^\s*<(!DOCTYPE|\w+)/) != nil
end
def scan_for_year(content)
content.each do |line|
if line =~ /Copyright ((\d+)(\s*,\s*\d+)*)/
return $1
elsif document_start?(line)
return YEAR
end
end
return YEAR
end
def update(path)
content = nil
dirty = false
File.open(path) { |file| content = file.readlines }
year = scan_for_year(content)
copyright_comment = synthesize_copyright(@template, year)
# Ignore the first line, it is expected to be the <?xml
# directive.
0.upto(@template.length() - 1) do |line|
dirty ||= content[line + 1] != copyright_comment[line]
end
# TODO: What if the new comment is *shorter* than the old comment?
# Need to find and trim those line.
return false if !dirty
until document_start?(content[1])
content.delete_at(1)
end
content.insert(1, *copyright_comment)
write_file(path, content)
return true
end
end
# Maps a particular file path pattern to a particular filter. Tracks the files
# that have matched the pattern.
class FilterPattern
def initialize(pattern, filter)
@pattern = pattern
@filter = filter
@files = []
end
def match?(path)
if path.match(@pattern) != nil
@files << path
return true
end
return false
end
def update
count = 0
@files.each do |file|
count += 1 if @filter.update(file)
end
return count
end
end
$filter_patterns = []
def register_filter(pattern, filter)
$filter_patterns << FilterPattern.new(pattern, filter)
end
def match?(path)
$filter_patterns.each do |fp|
return true if fp.match?(path)
end
return false
end
register_filter(/\.(java|aj)$/, JavaFilter.new)
register_filter(/(\/cli\.xconf|(\.(xml|xsl|jwc|application|library|page|script)))$/, XMLFilter.new)
register_filter(/\.properties$/, PropertiesFilter.new)
$matches = 0
$update_count = 0
if (ARGV[0] == "-o")
ARGV.shift
$ORG = ARGV.shift
puts "Using organization '#$ORG'"
end
Find.find(*ARGV) do |f|
if f =~ /(CVS|SVN|target)$/
Find.prune
else
$matches += 1 if match?(f)
end
end
$filter_patterns.each { |fp| $update_count += fp.update }
puts "Updated #$update_count files (of #$matches files found)."