| # 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. |
| |
| module Buildr #:nodoc: |
| |
| # A filter knows how to copy files from one directory to another, applying mappings to the |
| # contents of these files. |
| # |
| # You can specify the mapping using a Hash, and it will map ${key} fields found in each source |
| # file into the appropriate value in the target file. For example: |
| # |
| # filter.using 'version'=>'1.2', 'build'=>Time.now |
| # |
| # will replace all occurrences of <tt>${version}</tt> with <tt>1.2</tt>, and <tt>${build}</tt> |
| # with the current date/time. |
| # |
| # You can also specify the mapping by passing a proc or a method, that will be called for |
| # each source file, with the file name and content, returning the modified content. |
| # |
| # Without any mapping, the filter simply copies files from the source directory into the target |
| # directory. |
| # |
| # A filter has one target directory, but you can specify any number of source directories, |
| # either when creating the filter or calling #from. Include/exclude patterns are specified |
| # relative to the source directories, so: |
| # filter.include '*.png' |
| # will only include PNG files from any of the source directories. |
| # In the same way, you can use regular expressions, so: |
| # filter.include /picture_.*\.png/ |
| # will only include PNG files starting with picture_ from any of the sources directories. |
| # |
| # See Buildr#filter. |
| class Filter |
| |
| def initialize #:nodoc: |
| clear |
| end |
| |
| # Returns the list of source directories (each being a file task). |
| attr_reader :sources |
| |
| # :call-seq: |
| # clear => self |
| # |
| # Clear filter sources and include/exclude patterns |
| def clear |
| @include = [] |
| @exclude = [] |
| @sources = FileList[] |
| @mapper = Mapper.new |
| self |
| end |
| |
| # :call-seq: |
| # from(*sources) => self |
| # |
| # Adds additional directories from which to copy resources. |
| # |
| # For example: |
| # filter.from('src').into('target').using('build'=>Time.now) |
| def from(*sources) |
| @sources |= sources.flatten.map { |dir| file(File.expand_path(dir.to_s)) } |
| self |
| end |
| |
| # The target directory as a file task. |
| def target |
| return nil unless @target_dir |
| unless @target |
| @target = file(File.expand_path(@target_dir)) { |task| run if @target == task } |
| @target.enhance @include.select {|f| f.is_a?(Rake::FileTask)} |
| @target.enhance @exclude.select {|f| f.is_a?(Rake::FileTask)} |
| @target.enhance copy_map.values |
| end |
| @target |
| end |
| |
| # :call-seq: |
| # into(dir) => self |
| # |
| # Sets the target directory into which files are copied and returns self. |
| # |
| # For example: |
| # filter.from('src').into('target').using('build'=>Time.now) |
| def into(dir) |
| @target_dir = dir.to_s |
| @target = nil |
| self |
| end |
| |
| # :call-seq: |
| # include(*files) => self |
| # |
| # Specifies files to include and returns self. See FileList#include. |
| # |
| # By default all files are included. You can use this method to only include specific |
| # files from the source directory. |
| def include(*files) |
| @include += files.flatten |
| self |
| end |
| alias :add :include |
| |
| # :call-seq: |
| # exclude(*files) => self |
| # |
| # Specifies files to exclude and returns self. See FileList#exclude. |
| def exclude(*files) |
| @exclude += files.flatten |
| self |
| end |
| |
| # The mapping. See #using. |
| def mapping #:nodoc: |
| @mapper.config |
| end |
| |
| # The mapper to use. See #using. |
| def mapper #:nodoc: |
| @mapper.mapper_type |
| end |
| |
| # :call-seq: |
| # using(mapping) => self |
| # using { |file_name, contents| ... } => self |
| # |
| # Specifies the mapping to use and returns self. |
| # |
| # The most typical mapping uses a Hash, and the default mapping uses the Maven style, so |
| # <code>${key}</code> are mapped to the values. You can change that by passing a different |
| # format as the first argument. Currently supports: |
| # * :ant -- Map <code>@key@</code>. |
| # * :maven -- Map <code>${key}</code> (default). |
| # * :ruby -- Map <code>#{key}</code>. |
| # * :erb -- Map <code><%= key %></code>. |
| # * Regexp -- Maps the matched data (e.g. <code>/=(.*?)=/</code> |
| # |
| # For example: |
| # filter.using 'version'=>'1.2' |
| # Is the same as: |
| # filter.using :maven, 'version'=>'1.2' |
| # |
| # You can also pass a proc or method. It will be called with the file name and content, |
| # to return the mapped content. |
| # |
| # Without any mapping, all files are copied as is. |
| # |
| # To register new mapping type see the Mapper class. |
| def using(*args, &block) |
| @mapper.using(*args, &block) |
| self |
| end |
| |
| # :call-seq: |
| # run => boolean |
| # |
| # Runs the filter. |
| def run |
| copy_map = copy_map() |
| |
| mkpath target.to_s |
| return false if copy_map.empty? |
| |
| copy_map.each do |path, source| |
| dest = File.expand_path(path, target.to_s) |
| if File.directory?(source) |
| mkpath dest |
| else |
| mkpath File.dirname(dest) |
| if @mapper.mapper_type |
| mapped = @mapper.transform(File.open(source, 'rb') { |file| file.read }, path) |
| File.open(dest, 'wb') { |file| file.write mapped } |
| else # no mapping |
| cp source, dest |
| end |
| end |
| File.chmod(File.stat(source).mode | 0200, dest) |
| end |
| touch target.to_s |
| true |
| end |
| |
| # Returns the target directory. |
| def to_s |
| target.to_s |
| end |
| |
| protected |
| |
| # :call-seq: |
| # pattern_match(file, pattern) => boolean |
| # |
| # This method returns true if the file name matches the pattern. |
| # The pattern may be a String, a Regexp or a Proc. |
| # |
| def pattern_match(file, pattern) |
| case |
| when pattern.is_a?(Regexp) |
| return file.match(pattern) |
| when pattern.is_a?(String) |
| return File.fnmatch(pattern, file) |
| when pattern.is_a?(Proc) |
| return pattern.call(file) |
| when pattern.is_a?(Rake::FileTask) |
| return pattern.to_s.match(file) |
| else |
| raise "Cannot interpret pattern #{pattern}" |
| end |
| end |
| |
| private |
| def copy_map |
| sources.each { |source| raise "Source directory #{source} doesn't exist" unless File.exist?(source.to_s) } |
| raise 'No target directory specified, where am I going to copy the files to?' if target.nil? |
| |
| sources.flatten.map(&:to_s).inject({}) do |map, source| |
| files = Util.recursive_with_dot_files(source). |
| map { |file| Util.relative_path(file, source) }. |
| select { |file| @include.empty? || @include.any? { |pattern| pattern_match(file, pattern) } }. |
| reject { |file| @exclude.any? { |pattern| pattern_match(file, pattern) } } |
| files.each do |file| |
| src, dest = File.expand_path(file, source), File.expand_path(file, target.to_s) |
| map[file] = src if !File.exist?(dest) || File.stat(src).mtime >= File.stat(dest).mtime |
| end |
| map |
| end |
| end |
| |
| # This class implements content replacement logic for Filter. |
| # |
| # To register a new template engine @:foo@, extend this class with a method like: |
| # |
| # def foo_transform(content, path = nil) |
| # # if this method yields a key, the value comes from the mapping hash |
| # content.gsub(/world/) { |str| yield :bar } |
| # end |
| # |
| # Then you can use :foo mapping type on a Filter |
| # |
| # filter.using :foo, :bar => :baz |
| # |
| # Or all by your own, simply |
| # |
| # Mapper.new(:foo, :bar => :baz).transform("Hello world") # => "Hello baz" |
| # |
| # You can handle configuration arguments by providing a @*_config@ method like: |
| # |
| # # The return value of this method is available with the :config accessor. |
| # def moo_config(*args, &block) |
| # raise ArgumentError, "Expected moo block" unless block_given? |
| # { :moos => args, :callback => block } |
| # end |
| # |
| # def moo_transform(content, path = nil) |
| # content.gsub(/moo+/i) do |str| |
| # moos = yield :moos # same than config[:moos] |
| # moo = moos[str.size - 3] || str |
| # config[:callback].call(moo) |
| # end |
| # end |
| # |
| # Usage for the @:moo@ mapper would be something like: |
| # |
| # mapper = Mapper.new(:moo, 'ooone', 'twoo') do |str| |
| # i = nil; str.capitalize.gsub(/\w/) { |s| s.send( (i = !i) ? 'upcase' : 'downcase' ) } |
| # end |
| # mapper.transform('Moo cow, mooo cows singing mooooo') # => 'OoOnE cow, TwOo cows singing MoOoOo' |
| class Mapper |
| |
| attr_reader :mapper_type, :config |
| |
| def initialize(*args, &block) #:nodoc: |
| using(*args, &block) |
| end |
| |
| def using(*args, &block) |
| case args.first |
| when Hash # Maven hash mapping |
| using :maven, *args |
| when Binding # Erb binding |
| using :erb, *args |
| when Symbol # Mapping from a method |
| raise ArgumentError, "Unknown mapping type: #{args.first}" unless respond_to?("#{args.first}_transform", true) |
| configure(*args, &block) |
| when Regexp # Mapping using a regular expression |
| raise ArgumentError, 'Expected regular expression followed by mapping hash' unless args.size == 2 && Hash === args[1] |
| @mapper_type, @config = *args |
| else |
| unless args.empty? && block.nil? |
| raise ArgumentError, 'Expected proc, method or a block' if args.size > 1 || (args.first && block) |
| @mapper_type = :callback |
| config = args.first || block |
| raise ArgumentError, 'Expected proc, method or callable' unless config.respond_to?(:call) |
| @config = config |
| end |
| end |
| self |
| end |
| |
| BINARY_FILES = [ '*.png', '*.gif', '*.jpg', '*.jpeg' ] |
| |
| def is_binary?(content, path) |
| !!path && BINARY_FILES.any? { |glob| File.fnmatch(glob, path) } |
| end |
| |
| def transform(content, path = nil) |
| return content if is_binary?(content, path) |
| type = Regexp === mapper_type ? :regexp : mapper_type |
| raise ArgumentError, "Invalid mapper type: #{type.inspect}" unless respond_to?("#{type}_transform", true) |
| self.__send__("#{type}_transform", content, path) { |key| config[key] || config[key.to_s.to_sym] } |
| end |
| |
| private |
| def configure(mapper_type, *args, &block) |
| configurer = method("#{mapper_type}_config") rescue nil |
| if configurer |
| @config = configurer.call(*args, &block) |
| else |
| raise ArgumentError, "Missing hash argument after :#{mapper_type}" unless args.size == 1 && Hash === args[0] |
| @config = {} unless Hash === @config |
| args.first.each_pair { |k, v| @config[k] = v.to_s } |
| end |
| @mapper_type = mapper_type |
| end |
| |
| def maven_transform(content, path = nil) |
| content.gsub(/\$\{.*?\}/) { |str| yield(str[2..-2]) || str } |
| end |
| |
| def ant_transform(content, path = nil) |
| content.gsub(/@.*?@/) { |str| yield(str[1..-2]) || str } |
| end |
| |
| def ruby_transform(content, path = nil) |
| content.gsub(/#\{.*?\}/) { |str| yield(str[2..-2]) || str } |
| end |
| |
| def regexp_transform(content, path = nil) |
| content.gsub(mapper_type) { |str| yield(str.scan(mapper_type).join) || str } |
| end |
| |
| def callback_transform(content, path = nil) |
| config.call(path, content) |
| end |
| |
| def erb_transform(content, path = nil) |
| case config |
| when Binding |
| bnd = config |
| when Hash |
| bnd = OpenStruct.new |
| table = config.inject({}) { |h, e| h[e.first.to_sym] = e.last; h } |
| bnd.instance_variable_set(:@table, table) |
| bnd = bnd.instance_eval { binding } |
| else |
| bnd = config.instance_eval { binding } |
| end |
| ERB.new(content).result(bnd) |
| end |
| |
| def erb_config(*args, &block) |
| if block_given? |
| raise ArgumentError, "Expected block or single argument, but both given." unless args.empty? |
| block |
| elsif args.size > 1 |
| raise ArgumentError, "Expected block or single argument." |
| else |
| args.first |
| end |
| end |
| |
| end # class Mapper |
| |
| end |
| |
| # :call-seq: |
| # filter(*source) => Filter |
| # |
| # Creates a filter that will copy files from the source directory(ies) into the target directory. |
| # You can extend the filter to modify files by mapping <tt>${key}</tt> into values in each |
| # of the copied files, and by including or excluding specific files. |
| # |
| # A filter is not a task, you must call the Filter#run method to execute it. |
| # |
| # For example, to copy all files from one directory to another: |
| # filter('src/files').into('target/classes').run |
| # To include only the text files, and replace each instance of <tt>${build}</tt> with the current |
| # date/time: |
| # filter('src/files').into('target/classes').include('*.txt').using('build'=>Time.now).run |
| def filter(*sources) |
| Filter.new.from(*sources) |
| end |
| |
| end |