Branding und Customizing einer JRuby on Rails Anwendung

Wenn man Rails-Anwendungen für Kunden betreibt, muss man frühzeitig an kundenspezifische Anpassungen denken. Neben dem kundenspezifischen Design (Farben und Logos aus dem Corporate Design, "Branding") spielen dabei auch konfigurierbare Feature-Sets ("Customizing") eine wichtige Rolle. Natürlich wird jeder Kunde sowohl seine eigene Datenbank bekommen als auch seinen eigenen Rails-Server: Sicherheit geht vor! Jedoch will man als Entwickler nicht deswegen mehrere Kopien des Quelltextes auf dem aktuellen Stand halten müssen. In diesem Artikel wollen wir unser Branding-Konzept am Beispiel unserer Zeiterfassung ObjectTime vorstellen. An den Stellen, wo wir es einsetzen, hat es sich bereits ausgezahlt.
Um ein kundenspezifisches Layout und Funktionen zu erreichen, haben wir uns entschlossen die Funktionalität des Warbler Gems zu nutzen, um kundenspezifische war-Dateien beim Build-Prozess zu erhalten. Außerdem können mittels einer Konfigurationsdatei names feature_settings.rb bestimmte Features ein- oder ausgeschaltet werden.

Wie funktioniert das nun genau?

1. Verzeichnisstruktur Anpassungen

Branding verzeichnisstruktur Wir erweitern die typische Rails Verzeichnisstruktur um ein /branding Verzeichnis. Die nächste Verzeichnisebene benennt die Namen unserer kundenspezifischen Anwendungen. In unserem Beispiel haben wir also zwei kundenspezifische Anwendungen acme_orange_color und acme_zeiterfassung.

Wichtig: Diese Namen sind gleichzeitig die Namen der später erzeugten war-Dateien!

Innerhalb dieser einzelnen Anwendungs-"Branding"-Verzeichnisse werden nun alle Dateien, analog zur typischen Rails-Verzeichnisstruktur, hinterlegt, die beim Bauen der kundenspezifischen Anwendung die Originalanwendungsdateien überschreiben sollen. So gibt es ein config Verzeichnis, in welchem alle für den Kunden zutreffenden Konfigurationen abgelegt werden und ein public Verzeichnis, in welchem die speziellen Bilder, angepassten Stylesheets und Javascript Dateien liegen. Es ist auch denkbar, dass es hier zusätzlich ein kundenspezifisches app/views Verzeichnis gibt, in dem man spezielle Views der Anwendung nach Kundenwunsch anpasst.
Im config Verzeichnis gibt es z.b.:
  • den jeweilige :session_key in der session_store.rb
  • die Datenbankverbindungen in der database.yml
  • die einzelnen Features werden in der feature_settings.rb ein- oder ausgeschaltet
  • und innerhalb des neuen Verzeichnisses locales_branding werden die kundenspezifischen I18N Übersetzungen abgelegt.
Im public Verzeichnis finden sich z.B.:
  • die kundenspezifischen Bilder in /public/images/
  • im Verzeichnis public/stylesheets/ liegen die print_branding.css und screen_branding.css (Die Einbindung dieser Dateien wird gleich genauer erläutert.)

2. Layout Anpassungen

Branding verzeichnisstruktur CSS Damit das kundenspezifischen Layout auch in der Anwendung angezeigt wird, haben wir in der layout.html.erb zusätzliche Stylesheets includiert: print_branding.css und screen_branding.css. Diese beiden Dateien existieren auch in der Standard Rails Verzeichnisstruktur, sind dort aber leer. Mit Leben gefüllt werden sie in den jeweiligen /branding Verzeichnissen.
WICHTIG: Die beiden *_branding.css Dateien müssen nach den Standard CSS Dateien aufgerufen werden.
<%= stylesheet_link_tag 'screen.css',
                            'print.css',
                            'screen_branding.css',
                            'print_branding.css',
                            :cache => true %>

3. Konfigurationsanpassungen

Damit wir auch die Internationalisierung der Anwendung kundenspezifisch gestalten können, gibt es das Verzeichnis locales_branding. Es ist in der Standard Rails Verzeichnisstruktur erneut leer und wird in den /branding Verzeichnissen gefüllt. Wir teilen Rails durch die Angabe folgender Zeilen in der environment.rb innerhalb des Rails::Initializer.run do |config| Blockes mit, dass für die I18N-Auswertung alle *.yml Dateien aus den Verzeichnissen config/locales (inklusive aller Unterverzeichnisse) genommen werden.

WICHTIG ist wieder die Aufrufreihenfolge!
Indem wir config/locales_branding erst nach config/locales der Railsumgebung bekannt machen, werden schon definierte Übersetzungen aus dem Verzeichnis config/locales mit kundenspezifischen aus dem config/locales_branding-Verzeichnis überschrieben.
  # The internationalization framework can be changed to have another default locale (standard is :en) or more load paths.
  # All files from config/locales/*.rb,yml are added automatically.
  config.i18n.load_path += Dir[File.join(RAILS_ROOT, 'config', 'locales', '**', '*.{rb,yml}')]
  config.i18n.load_path += Dir[File.join(RAILS_ROOT, 'config', 'locales_branding', '**', '*.{rb,yml}')]
  config.i18n.default_locale = :de
Desweiteren hatten wir anfangs des Tutorials erwähnt, dass auch bestimmte Features für Kunden ein- oder ausgeschaltet werden können. Diese Ein- oder Ausschalten wird in der Konfigurationsdatei feature_setting.rb erledigt. Dazu muss diese Datei in der environment.rb vor dem Rails::Initializer.run do |config| Block geladen werden.
# Set the global constants for feature enabling
require File.join(File.dirname(__FILE__), 'feature_settings')
Beispiel der feature_settings.rb:
# Feature Schalter

$FEATURE_LOGIN_INFO_TEXT         = false
$FEATURE_RESET_DB                = false
$FEATURE_SHOW_RESET_DB_COUNTDOWN = false

4. Rake Build Anpassungen

Soweit so gut! Nun kommt das eigentlich Spannende. Wir "mixen" unsere kundenspezifischen Anwendungsdatein zu einer war-Datei zusammen.

Im Standard Rails Verzeichnis config/ befindet sich unsere warble.rb Konfigurationsdatei. (Auf genauere Informationen zu den einzelnen Konfigurationsmöglichkeiten innerhalb der warble.rb gehen wir hier nicht ein. Mehr Infornationen findet man unter Warbler::Config). In dieser Datei definieren wir das "default" config.staging_dir. Diese Einstellung bezeichnet das Verzeichnis, in dem die Standarddateien für unsere war-Datei zusammengetragen werden. Unter Standarddateien verstehen wir hier alle Dateien, die zur erfolgreichen Ausführung der Anwendung innerhalb der war-Datei benötigt werden, ohne die speziellen kundenspezifischen Dateien aus dem /branding Verzeichnissen.
Warbler::Config.new do |config|
  # Temporary directory where the application is staged
  config.staging_dir = "tmp/war/default" 
  ...
  # Hier folgen alle weiteren Konfigurationen.
  ...


In unserer Build Datei Rakefile stellen wir nun spezielle Rake Tasks zur Erstellung der kundenspezifischen war-Dateien zur Verfügung.
namespace :zeiterfassung do
    CONFIG_DIR                    = "config"
    CUSTOMIZE_DIR                 = "branding"
    PUBLIC_DIR                    = "public"
    WEBINF_DIR                    = "WEB-INF"
    WAR_TMP_PATH                  = "tmp/war"
    DEFAULT_WAR_TMP_PATH          = "#{WAR_TMP_PATH}/default"
    WARBLER_CONFIG_FILE           = "warble.rb"
    URLREWRITE_FILE               = "urlrewrite.xml"
    NO_BRANDING_DIRS              = %w[ . .. .svn ]
  
  desc "Build customize WAR Files"
  task :branding_war_files => ["war:app", "war:public", "war:webxml"] do    
    Dir.foreach(CUSTOMIZE_DIR) do |d|
      if File.directory?("#{CUSTOMIZE_DIR}/#{d}") && !(NO_BRANDING_DIRS.include? d)
        puts "Start build #{d}"
        customize_app_path = File.join(RAILS_ROOT, CUSTOMIZE_DIR, "#{d}")
        war_tmp_path       = File.join(RAILS_ROOT, WAR_TMP_PATH, "#{d}")
        # tmp und Log Verzeichnisse anlegen
        Rake::Task['zeiterfassung:create_tmp_and_log_dir'].invoke

        # default Dateien kopieren
        cp_r(File.join(RAILS_ROOT, DEFAULT_WAR_TMP_PATH), war_tmp_path)

        # Copy all customize Files
        cp_r(File.join(customize_app_path, CONFIG_DIR, "."),  File.join(war_tmp_path, WEBINF_DIR, CONFIG_DIR))
        cp_r(File.join(customize_app_path, PUBLIC_DIR, "."),  war_tmp_path)
               
        warble_namespace = "#{d}"
        config = Warbler::Config.new do |config|
          config.staging_dir = WAR_TMP_PATH + "/#{d}"
          config.war_name = "#{d}"
        end
        Warbler::Task.new(warble_namespace, config)
        Rake::Task[warble_namespace+":jar"].invoke
      end
    end
  end
    
  desc "create tmp and log Dir in war file"
  task :create_tmp_and_log_dir do
    path = File.join(RAILS_ROOT, DEFAULT_WAR_TMP_PATH, WEBINF_DIR)
    cd path
    makedirs "tmp"
    makedirs "log"
    cd RAILS_ROOT
  end
 
end
Was passiert hier im Einzelnen?
Zuerst wird für die Anwendung ein eigener namespace angelegt. Der Task :branding_war_files erledigt nun die ganzen Aufgaben. Bei seiner Ausführung werden zuerst die Tasks "war:app", "war:public", "war:webxml" hintereindander ausgeführt. Diese Tasks sind Standdardtasks des Warbler Gems. Der namespace "war:" kommt durch die warble.rb Konfigurationsdatei im Standard Rails /configVerzeichnis. Beim initialen Laden der Railsumgebung wird automatisch ein namespace "war:" mit den Konfigurationseinstellungen aus der warble.rb erzeugt.
  • war:app und war:public kopiert alle Anwendungsdateien (auch benötigte Libs und Gems) in das vorher angegebene staging_dir
  • war:webxml generiert die web.xml Datei für die Anwednung
Nachdem nun im Verzeichnis DEFAULT_WAR_TMP_PATH == tmp/war/default die Standarddateien für unsere war-Datei zusammengetragen wurden beginnt die eigentliche kundenspezifische Anpassung.

Die Schleife Dir.foreach(CUSTOMIZE_DIR) do |d| ... end durchläuft alle direkten Unterverzeichnisse des /branding Verzeichnisses und erstellt für jedes dieser Verzeichnisse eine war-Datei. In unserem Beispiel sind es die beiden Verzeichnisse acme_orange_color und acme_zeiterfassung. Für jede dieser beiden Anwendungen werden die Dateien aus dem DEFAULT_WAR_TMP_PATH in die kundenspezifischen Verzeichnisse tmp/war/acme_orange_color und tmp/war/acme_zeiterfassung kopiert und anschliessend die Dateien aus den jeweiligen branding Verzeichnis darüber gelegt. In Zeile 29 des Task Beispieles wird für jede kundenspezifische Anwendung ein neuer namespace definiert, um den mit dem Warbler Gem kommenden namespace:jar Task zum Packen der Anwendung nutzen zu können.

Mittels rake zeiterfassung:branding_war_files können die war-Dateien nun erstellt werden. Wir erhalten in unserem Beispiel 2 Dateien - acme_orange_color.war und acme_zeiterfassung.war www.jruby.org
Warbler Gem
rails-multisite Gem
Multi-Tenancy Made Easy von Tim Lossen
Writing Multi-Tenant Applications in Rails von Guy Naor [Video]
Multitenancy
Corporate Design