require 'trinidad/configuration' module Trinidad class WebApp @@defaults = Trinidad::Configuration::DEFAULTS attr_reader :config, :default_config def self.create(config, default_config = Trinidad.configuration) war?(config, default_config) ? WarWebApp.new(config, default_config) : rackup?(config, default_config) ? RackupWebApp.new(config, default_config) : RailsWebApp.new(config, default_config) end def initialize(config, default_config) @config, @default_config = config, default_config || {} complete_config! # NOTE: we should maybe @config.freeze here ?! end def [](key) key = key.to_sym config.key?(key) ? config[key] : default_config[key] end def key?(key, use_default = true) key = key.to_sym return true if config.has_key?(key) use_default ? default_config.key?(key) : false end %w{ root_dir jruby_compat_version rackup log async_supported reload_strategy }.each do |method| class_eval "def #{method}; self[:'#{method}']; end" end alias_method :web_app_dir, :root_dir # is getting deprecated soon def context_path path = self[:context_path] || self[:path] path ? path.to_s : path end def context_name name = self[:context_name] || self[:name] name ? name.to_s : name end # NOTE: should be set to application root (base) directory thus # JRuby-Rack correctly resolves relative paths for the context! def doc_base; self[:doc_base] || root_dir; end def jruby_min_runtimes if min = config[:jruby_min_runtimes] return min.to_i # min specified overrides :threadsafe else # but :threadsafe takes precendence over default : self[:threadsafe] ? 1 : default_config[:jruby_min_runtimes] end end def jruby_max_runtimes if max = config[:jruby_max_runtimes] return max.to_i # max specified overrides :threadsafe else # but :threadsafe takes precendence over default : self[:threadsafe] ? 1 : default_config[:jruby_max_runtimes] end end def jruby_runtime_acquire_timeout self[:jruby_runtime_acquire_timeout] || 5.0 # default 10s seems too high end def environment; self[:environment] || @@defaults[:environment]; end # TODO check web.xml def public_dir @public_dir ||= expand_path(public_root) end # by (a "Rails") convention use '[RAILS_ROOT]/tmp' def work_dir @work_dir ||= self[:work_dir] || File.join(root_dir, 'tmp') end # by a "Rails" convention defaults to '[RAILS_ROOT]/log' def log_dir @log_dir ||= self[:log_dir] || File.join(root_dir, 'log') end def monitor File.expand_path(self[:monitor] || 'restart.txt', work_dir) end def context_xml; self[:context_xml] || self[:default_context_xml]; end def web_xml; self[:web_xml] || self[:default_web_xml]; end def default_web_xml; self[:default_web_xml]; end def java_lib # accepts #deprecated :libs_dir syntax self[:java_lib] || self[:libs_dir] || @@defaults[:java_lib] end def java_classes # accepts #deprecated :classes_dir syntax self[:java_classes] || self[:classes_dir] || File.join(java_lib, 'classes') end def java_lib_dir @java_lib_dir ||= self[:java_lib_dir] || expand_path(java_lib) end alias_method :libs_dir, :java_lib_dir # #deprecated def java_classes_dir @java_classes_dir ||= self[:java_classes_dir] || expand_path(java_classes) end alias_method :classes_dir, :java_classes_dir # #deprecated def extensions @extensions ||= begin extensions = default_config[:extensions] || {} extensions.merge(config[:extensions] || {}) end end def context_params @context_params ||= {} add_context_param 'jruby.min.runtimes', jruby_min_runtimes add_context_param 'jruby.max.runtimes', jruby_max_runtimes add_context_param 'jruby.initial.runtimes', jruby_min_runtimes add_context_param 'jruby.runtime.acquire.timeout', jruby_runtime_acquire_timeout add_context_param 'jruby.compat.version', jruby_compat_version || RUBY_VERSION add_context_param 'public.root', File.join('/', public_root) @context_params end # @deprecated replaced with {#context_params} def init_params; context_params; end def add_context_param(param_name, param_value) @context_params ||= {} if ! param_value.nil? && ! web_xml_context_param(param_name) @context_params[param_name] = param_value.to_s end end def deployment_descriptor return nil if @deployment_descriptor == false @deployment_descriptor ||= expand_path(web_xml) || false end # @deprecated use {#deployment_descriptor} def default_deployment_descriptor return nil if @default_deployment_descriptor == false @default_deployment_descriptor ||= expand_path(default_web_xml) || false end def public_root @public_root ||= ( public_config[:root] || @@defaults[:public] ) end alias_method :public, :public_root # we do support nested :public configuration e.g. : # public: # root: /assets # cache: true # cache_ttl: 60000 def public_config @public_config ||= self[:public].is_a?(String) ? { :root => self[:public] } : ( self[:public] || {} ) end def aliases # :public => { :aliases => ... } return nil unless aliases = ( self[:aliases] || public_config[:aliases] ) return aliases if aliases.is_a?(String) # "/aliasPath1=docBase1,/aliasPath2=docBase2" @aliases ||= aliases.map do |path, base| path = path.to_s if (root = '/') != path[0, 1] path = (root << path) end "#{path}=#{File.expand_path(base, root_dir)}" end.join(',') end def caching_allowed? # :public => { :cached => ... } # ((BaseDirContext) resources).setCached(isCachingAllowed()) return @caching_allowed unless @caching_allowed.nil? @caching_allowed = self[:caching_allowed] if @caching_allowed.nil? @caching_allowed = public_config[:cached] if @caching_allowed.nil? @caching_allowed = environment != 'development' end end @caching_allowed = !! @caching_allowed end # The cache max size in kB def cache_max_size # :public => { :cache_max_size => ... } # ((BaseDirContext) resources).setCacheMaxSize self[:cache_max_size] || public_config[:cache_max_size] end # The max size for a cached object in kB def cache_object_max_size # :public => { :cache_object_max_size => ... } # ((BaseDirContext) resources).setCacheObjectMaxSize self[:cache_object_max_size] || public_config[:cache_object_max_size] end # Cache entry time-to-live in millis def cache_ttl # :public => { :cache_ttl => ... } # ((BaseDirContext) resources).setCacheTTL self[:cache_ttl] || public_config[:cache_ttl] end def class_loader @class_loader ||= org.jruby.util.JRubyClassLoader.new(JRuby.runtime.jruby_class_loader) end def class_loader! ( @class_loader = nil ) || class_loader end # @deprecated replaced with {#class_loader!} def generate_class_loader; class_loader!; end def define_lifecycle Trinidad::Lifecycle::WebApp::Default.new(self) end # Reset the hold web application state so it gets re-initialized. # Please note that the configuration objects are not cleared. def reset! vars = instance_variables.map(&:to_sym) vars = vars - [ :'@config', :'@default_config' ] vars.each { |var| instance_variable_set(var, nil) } end DEFAULT_SERVLET_CLASS = nil # by default we resolve by it's name DEFAULT_SERVLET_NAME = 'default' # Returns a servlet config for the DefaultServlet. # This servlet is setup for each and every Tomcat context and is named # 'default' and mapped to '/' we allow fine tunning of this servlet. # Return values should be interpreted as follows : # true - do nothing leave the servlet as set-up (by default) # false - remove the set-up default (e.g. configured in web.xml) def default_servlet return @default_servlet unless @default_servlet.nil? @default_servlet ||= begin if ! web_xml_servlet?(DEFAULT_SERVLET_CLASS, DEFAULT_SERVLET_NAME) default_servlet = self[:default_servlet] if default_servlet.is_a?(javax.servlet.Servlet) { :instance => default_servlet } elsif default_servlet == false false # forced by user to remove elsif default_servlet == true true # forced by user to leave as is else default_servlet = {} if default_servlet.nil? unless default_servlet.key?(:class) # we use a custom class by default to server /public assets : default_servlet[:class] = 'rb.trinidad.servlets.DefaultServlet' end default_servlet end else false # configured in web.xml thus remove the (default) "default" end end end JSP_SERVLET_CLASS = nil # by default we resolve by it's name JSP_SERVLET_NAME = 'jsp' # Returns a servlet config for the JspServlet. # This servlet is setup by default for every Tomcat context and is named # 'jsp' with '*.jsp' and '*.jspx' mappings. # Return values should be interpreted as follows : # true - do nothing leave the servlet as set-up (by default) # false - remove the set-up servlet (by default we do not need jsp support) def jsp_servlet return @jsp_servlet unless @jsp_servlet.nil? @jsp_servlet ||= begin if ! web_xml_servlet?(JSP_SERVLET_CLASS, JSP_SERVLET_NAME) jsp_servlet = self[:jsp_servlet] if jsp_servlet.is_a?(javax.servlet.Servlet) { :instance => jsp_servlet } else jsp_servlet || false # remove jsp support unless specified end else false # configured in web.xml thus remove the default "jsp" end end end RACK_SERVLET_CLASS = 'org.jruby.rack.RackServlet' RACK_SERVLET_NAME = 'rack' # in-case of a "custom" rack servlet class RACK_FILTER_CLASS = 'org.jruby.rack.RackFilter' RACK_FILTER_NAME = 'rack' # Returns a config for the RackServlet or nil if no need to set-up one. # (to be used for dispatching to this Rack / Rails web application) def rack_servlet return nil if @rack_servlet == false @rack_servlet ||= begin rack_servlet = self[:rack_servlet] || self[:servlet] || {} if rack_servlet.is_a?(javax.servlet.Servlet) { :instance => rack_servlet, :name => RACK_SERVLET_NAME, :mapping => '/*' } else servlet_class = rack_servlet[:class] || RACK_SERVLET_CLASS servlet_name = rack_servlet[:name] || RACK_SERVLET_NAME if ! web_xml_servlet?(servlet_class, servlet_name) && ! web_xml_filter?(RACK_FILTER_CLASS, RACK_FILTER_NAME) { :instance => rack_servlet[:instance], :class => servlet_class, :name => servlet_name, :init_params => rack_servlet[:init_params], :async_supported => !! ( rack_servlet.has_key?(:async_supported) ? rack_servlet[:async_supported] : async_supported ), :load_on_startup => ( rack_servlet[:load_on_startup] || 2 ).to_i, :mapping => rack_servlet[:mapping] || '/*' } else if ! rack_servlet.empty? logger.info "ignoring :rack_servlet configuration for " + "#{context_path} due #{deployment_descriptor}" end false # no need to setup a rack servlet end end end || nil end # @deprecated use {#rack_servlet} instead def servlet; rack_servlet; end def rack_listener context_listener unless web_xml_listener?(context_listener) end def war?; self.class.war?(config); end def solo? ! is_a?(WarWebApp) && config[:solo] end def threadsafe? jruby_min_runtimes == 1 && jruby_max_runtimes == 1 end protected def context_listener raise NotImplementedError.new "context_listener expected to be redefined" end def complete_config! config[:root_dir] ||= self.class.root_dir(config, default_config) config[:context_path] = self.class.context_path(config, default_config) end public # Returns true if there's a servlet with the given servlet-class name # configured or if the optional name second argument is given it also # checks for a servlet with the given name. def web_xml_servlet?(servlet_class, servlet_name = nil) return nil unless web_xml_doc if servlet_class servlet_xpath = "/web-app/servlet[servlet-class = '#{servlet_class}']" return true if web_xml_doc.root.elements[servlet_xpath] # else try name end if servlet_name servlet_xpath = "/web-app/servlet[servlet-name = '#{servlet_name}']" return !! web_xml_doc.root.elements[servlet_xpath] end return false if servlet_class || servlet_name raise ArgumentError, "nor servlet_class nor servlet_name given" end # Returns true if a filter definition with a given filter-class is found. def web_xml_filter?(filter_class, filter_name = nil) return nil unless web_xml_doc if filter_class filter_xpath = "/web-app/filter[filter-class = '#{filter_class}']" return true if web_xml_doc.root.elements[filter_xpath] # else try name end if filter_name filter_xpath = "/web-app/filter[filter-name = '#{filter_name}']" return !! web_xml_doc.root.elements[filter_xpath] end return false if filter_class || filter_name raise ArgumentError, "nor filter_class nor filter_name given" end # Returns true if a listener definition with a given listener-class is found. def web_xml_listener?(listener_class) return nil unless web_xml_doc !! web_xml_doc.root.elements["/web-app/listener[listener-class = '#{listener_class}']"] end # Returns a param-value for a context-param with a given param-name. def web_xml_context_param(name) return nil unless web_xml_doc if param = web_xml_doc.root.elements["/web-app/context-param[param-name = '#{name}']"] param.elements['param-value'].text end end private def web_xml_doc return @web_xml_doc || nil unless @web_xml_doc.nil? descriptor = deployment_descriptor if descriptor && File.exist?(descriptor) begin require 'rexml/document' @web_xml_doc = REXML::Document.new(File.read(descriptor)) rescue REXML::ParseException => e logger.warn "invalid deployment descriptor:[#{descriptor}]\n #{e.message}" @web_xml_doc = false end @web_xml_doc || nil end end def expand_path(path) if path path_file = java.io.File.new(path) if path_file.absolute? path_file.absolute_path else File.expand_path(path, root_dir) end end end def logger @logger ||= Trinidad::Logging::LogFactory.getLog('') end protected def self.rackup?(config, default_config = nil) return true if config.has_key?(:rackup) root_dir = root_dir(config, default_config) config_ru = (default_config && default_config[:rackup]) || 'config.ru' # check for rackup (but still use config/environment.rb for rails 3) if File.exists?(File.join(root_dir, config_ru)) && ! rails?(config, default_config) # do not :rackup a rails app config[:rackup] = config_ru end config[:rackup] || ! Dir[File.join(root_dir, 'WEB-INF/**/config.ru')].empty? end def self.rails?(config, default_config = nil) root_dir = root_dir(config, default_config) # standart Rails 3.x `class Application < Rails::Application` if File.exists?(application = File.join(root_dir, 'config/application.rb')) return true if file_line_match?(application, /^[^#]*Rails::Application/) end if File.exists?(environment = File.join(root_dir, 'config/environment.rb')) return true if file_line_match?(environment) do |line| # customized Rails 3.x, expects a `Rails::Application` subclass # or a plain-old Rails 2.3 with `RAILS_GEM_VERSION = '2.3.14'` line =~ /^[^#]*Rails::Application/ || line =~ /^[^#]*RAILS_GEM_VERSION/ end end false end def self.war?(config, default_config = nil) config[:context_path] && config[:context_path].to_s[-4..-1] == '.war' end private def self.root_dir(config, default_config) # for backwards compatibility accepts the :web_app_dir "alias" config[:root_dir] || config[:web_app_dir] || ( default_config && ( default_config[:root_dir] || default_config[:web_app_dir] ) ) || Dir.pwd end def self.context_path(config, default_config = nil) path = config[:context_path] || ( default_config && default_config[:context_path] ) unless path name = config[:context_name] || ( default_config && default_config[:context_name] ) path = name.to_s == 'default' ? '/' : "/#{name}" end path = "/#{path}" if path.to_s[0, 1] != '/' path.to_s end def self.file_line_match?(path, pattern = nil) File.open(path) do |file| if block_given? file.each_line { |line| return true if yield(line) } else file.each_line { |line| return true if line =~ pattern } end end false end class Holder def initialize(web_app, context) @web_app, @context = web_app, context end attr_reader :web_app attr_accessor :context def monitor; web_app.monitor; end attr_accessor :monitor_mtime def try_lock locked? ? false : lock end def locked?; !!@lock; end def lock; @lock = true; end def unlock; @lock = false; end # #deprecated behaves Hash like for (<= 1.3.5) compatibility def [](key) case key.to_sym when :app then web_app when :context then context when :lock then @lock when :monitor then monitor when :mtime then monitor_mtime else raise NoMethodError, key.to_s end end # #deprecated behaves Hash like for (<= 1.3.5) compatibility def []=(key, val) case key.to_sym when :context then self.context=(val) when :lock then @lock = val when :mtime then self.monitor_mtime=(val) else raise NoMethodError, "#{key}=" end end end end # Rack web application (looks for a rackup config.ru file). class RackupWebApp < WebApp def context_params add_context_param 'rack.env', environment if rackup = self.rackup rackup = File.join(rackup, 'config.ru') if File.directory?(rackup) add_context_param 'rackup.path', rackup end super end def context_listener; 'org.jruby.rack.RackServletContextListener'; end end # Rails web app specifics. Supports the same versions as jruby-rack ! class RailsWebApp < WebApp def context_params add_context_param 'rails.env', environment add_context_param 'rails.root', '/' super end def context_listener; 'org.jruby.rack.rails.RailsServletContextListener'; end protected def complete_config! super # detect threadsafe! in config/environments/environment.rb : if ! key?(:threadsafe) && self.class.threadsafe?(root_dir, environment) config[:jruby_min_runtimes] = 1 unless key?(:jruby_min_runtimes, false) config[:jruby_max_runtimes] = 1 unless key?(:jruby_max_runtimes, false) end end private def self.threadsafe?(app_base, environment) threadsafe_match?("#{app_base}/config/environments/#{environment}.rb") || threadsafe_match?("#{app_base}/config/environment.rb") end def self.threadsafe_match?(file) File.exist?(file) && file_line_match?(file, /^[^#]*threadsafe!/) end end # A web application for deploying (java) .war files. class WarWebApp < WebApp def context_path super.gsub(/\.war$/, '') end def log_dir @log_dir ||= self[:log_dir] || File.join(work_dir, 'log') end def work_dir @work_dir ||= File.join(root_dir.gsub(/\.war$/, ''), 'WEB-INF') end def monitor File.expand_path(root_dir) end def define_lifecycle Trinidad::Lifecycle::WebApp::War.new(self) end end end