• 2012/6/9: コードの解析ツールをちょっと変更。CDATAセクション内のコードが<this.value>にも表示されてたのを修正。

XPagesのソースの読み方

XPagesのサンプルアプリの仕組みを理解するには、ソースコードを読めるようになるのが早道。

基本はXML

XPagesのソースコードはXMLで記述されている。

編集ボックスが一個だけ貼り付けられたXPageのソースコードはこんな感じ。

  01: <?xml version="1.0" encoding="UTF-8"?>
  02: <xp:view xmlns:xp="http://www.ibm.com/xsp/core">
  03:   <xp:inputText id="inputText1"></xp:inputText>
  04: </xp:view>
  
  • 1行目はXMLのお約束。あまり気にしなくていい
  • 2行目はXPage開始のお約束。02〜04がひとつのXPageになる。ここも気にしない。
  • 2行目が<xp:view>で始まり、4行目が</xp:view>で終わっているのがわかる。これらを「開始タグ」、「終了タグ」と呼ぶ。XMLはこのように必ず、開始タグと終了タグがペアになるのが特徴
  • 2行目の<xp:view>から4行目の</xp:view>で囲まれたブロックが「要素(Element)」になる。この場合は、「xp:view要素」と呼べばいいのだろう。XPageそのものを指す。
  • 3行目の「xp:inputText要素」は「xp:view要素」の中に含まれている。このようにXMLは要素が入れ子になっているという構造になる。
  • 3行目の「xp:inputText要素」は編集コントロールを示す。id="inputText"は「属性」と呼ばれるもので、どんな属性が使えるかは要素毎に異なる。 id属性は、要素を区別するためのIDとして使われる。
  • 3行目の<xp:inputText id="inputText1"></xp:inputText> は、<xp:inputText id="inputText1" /> と同義。一番最後を'/>'で止めることで、開始タグと終了タグを一度にかける

ボタンのソースコード

ボタンを押したら、ページを遷移するという処理のソースコード。

  01: <xp:button value="ラベル" id="button1">
  02:   <xp:eventHandler event="onclick" submit="true" refreshMode="complete">
  03:     <xp:this.action>
  04:       <xp:openPage name="/Test01.xsp" target="openDocument"></xp:openPage>
  05:     </xp:this.action>
  06:   </xp:eventHandler>
  07: </xp:button>
  
  • 1行目の「xp:button」要素にはvalueという属性があり、これでボタンの文字列をあらわす
  • 2行目の「xp:eventHandler」要素は、ボタンを押したときのイベントハンドラを示す。「イベントハンドラ」とは、あるイベントが発生したときに起動される処理のこと。
  • 2行目のevent属性に"onclick"とあるので、onclickイベントのイベントハンドラであることがわかる
  • 「xp:eventHandler」要素にはsubmit属性とreferMode属性があるのも読める。このへんはイベントプロパティをいじってみてどう変化するかで、どういう意味か読める
  • 3行目のthis.actionだけど、thisは親の要素を指す。つまるここではxp:eventHandler要素を指す。そのaction属性の設定をしている。つまり、<xp:eventHandler action="xxxx"> と意味的には同義になる。 この記述方法はよく出てくるので読み方を覚えておくといいだろう。
  • 4行目のxp:openPage要素は、name属性で指定されたXPageを開く。target属性で文書を開くモードを指定する

JavaScriptが組み込まれたソース

以下は、ラベルにDBのタイトルを表示するXPageのソース。

  01: <?xml version="1.0" encoding="UTF-8"?>
  02: <xp:view xmlns:xp="http://www.ibm.com/xsp/core">
  03:   <xp:label value="#{javascript:database.getTitle()}" id="label1"></xp:label>
  04: </xp:view>
  
  • 3行目はxp:label要素でラベルコントロールを示している。このvalue属性に"#{javascript:database.getTitle()}"という文字列がセットされている
  • これは、#{...}で囲まれた部分をサーバ側で実行し、その結果に置き換えるという意味になる。よって、ブラウザで表示が完了したあとに、HTMLのソースを見ても単に"基本App"のような文字列がセットされているだけになる。
  • このソースを以下のように記述する場合もある。上のコードと下のコードは同義になる。

      01: <?xml version="1.0" encoding="UTF-8"?>
      02: <xp:view xmlns:xp="http://www.ibm.com/xsp/core">
      03:   <xp:label id="label1">
      04:     <xp:this.value>
      05:       <![CDATA[#{javascript:database.getTitle()}]]>
      06:     </xp:this.value>
      07:   </xp:label>
      08: </xp:view>
      

value="#{...}"の場合、組み込むコードには"が使えない。使うには\"のようにエスケープする必要がある。 2番目の方法は、<![CDATA[...]]>を使っているが、こちらは"が普通に使える。これはXMLで決められている記述方法で、「CDATAセクション」とよぶものらしい。

あと、複数行にわたるコードを組み込む場合は、<![CDATA[...]]>を使うほうが読みやすい。どちらも適当に改行などを入れて読みやすくすることはできる。

データソース関係のソースコード

データソースを使っているページもやや特殊なので、一例をあげて説明してみる。

  01: <?xml version="1.0" encoding="UTF-8"?>
  02: <xp:view xmlns:xp="http://www.ibm.com/xsp/core">
  03:   <xp:this.data>
  04:     <xp:dominoDocument var="dominoDocument1" formName="MainForm" />
  05:   </xp:this.data>  
  06:   <xp:table>
  07:     <xp:tr>
  08:       <xp:td>
  09:         <xp:label value="Hostname:" id="hostname_Label1" for="hostname1" />
  10:       </xp:td>
  11:       <xp:td>
  12:         <xp:inputText value="#{dominoDocument1.hostname}" id="hostname1" />
  13:       </xp:td>
  14:     </xp:tr>
  15:     <xp:tr>
  16:       <xp:td>
  17:         <xp:label value="Pcid:" id="pcid_Label1" for="pcid1" />
  18:       </xp:td>
  19:       <xp:td>
  20:         <xp:inputText value="#{dominoDocument1.pcid}" id="pcid1" />
  21:       </xp:td>
  22:     </xp:tr>
  23:   </xp:table>
  24: </xp:view>
  
  • 3〜5行目でNotes文書式のデータソースの定義をしている。var属性で指定されいるのが、データソースを指す変数のようなもの
  • 6〜23にはHTMLのテーブルでよく使うタグが出てくる。このようにXPage上では普通のHTMLも組み込める
  • 12行目や20行目がデータソースを使っている部分。編集ボックスコントロールのvalue属性に"#{dominoDocument1.hostname}"のようなものが指定されている。
  • dominoDocument1は、4行目で定義されたデータソースを指す変数のようなもの。dominoDocument1.hostnameはデータソースがあるフォームに紐付けられているので、フォーム上の[hostname]フィールドに 紐づいていることをしてしている。このXPageを開くときに、データソースがある特定のNotes文書を指しているなら、"#{dominoDocument1.hostname}"はその文書の[hostname]フィールドが 保持している文字列を返すという具合になる。
  • ただ、12行目や20行目に出てくるvalue="#{...}"という処理だが、先ほどの例に出てきたvalue="#{javascript:...}"とちょっと書き方が違うのが気になるのだが、どういう違いがあるのかは正確にはわからん

ソースコードを整形するツール

XPageのソースは適当なところで改行されてたり、JavaScriptのコードのインデントがずれてたりしてて、読みやすいとはいえない。

そこで、ソースコードを整形して、自分好みの形に整形してみる。 こんな感じに整形できる。

  # 整形前
  <xp:panel themeId="Panel.header">
          <xp:text escape="true" themeId="ComputedField.header.subtitle"
                  id="cfAuthorProfileTitle">
                  <xp:this.value><![CDATA[#{javascript:var cc             = getComponent("ccProfileHeader").getPropertyMap();
  var name        = cc.getProperty("name");
  var format      = res.getString("authorprofile.title");
  
  if (name == null) {
      name = cc.getProperty("nameAbbreviated");
  }
  
  I18n.format(format, name);}]]></xp:this.value>
          </xp:text>
  </xp:panel>
  

 

  # 整形後
  <xp:panel themeId="Panel.header">
    <xp:text escape="true" id="cfAuthorProfileTitle" themeId="ComputedField.header.subtitle">
      <xp:this.value>
        <![CDATA[
          #{javascript:
            var cc             = getComponent("ccProfileHeader").getPropertyMap();
            var name        = cc.getProperty("name");
            var format      = res.getString("authorprofile.title");
  
            if (name == null) {
                    name = cc.getProperty("nameAbbreviated");
            }
  
            I18n.format(format, name);
          }
        ]]>
      </xp:this.value>
    </xp:text>
  </xp:panel>
  

整形するツールはRubyで書いた。xmlをsample.xmlとかにコピーし、ruby analize.rb sample.txt のように使う。 エラー処理は省いているので、開始タグがぬけているような不完全なXMLではエラーになる。文字コードも合わせておかないとうまくいかないかも。 JavaScriptのコードは整形できてない。

  require "rexml/document"
  include REXML
  
  def analize_cdatas(cdatas)
    lines = []
    cdatas.each {|cdata|
      lines.push "<![CDATA["
  
      code_str = cdata.to_s
      if code_str =~ /^\#\{javascript\:(.+)\}$/m
        js_code_str = $1
  
        lines.push "  #\{javascript:"
        js_code_str.split(/\n/).each {|oneline|
          oneline.gsub!(/\t/, "  ")
          lines.push "    #{oneline}"
        }
        lines.push "  }"
      else
        lines.push "  #{cdata.to_s}"
      end
      lines.push "]]>"
    }
    lines
  end
  
  def show_element(element, level)
    sp = '  ' * level
  
    if element.has_attributes?
      attr_list = []
      element.attributes.each {|name, value|
        attr_list.push "#{name}=\"#{value}\""
      }
      open_tag = "#{sp}<#{element.expanded_name} #{attr_list.join(' ')}>"
  
    else
      open_tag = "#{sp}<#{element.expanded_name}>"
    end
    if (element.text) and (element.text.gsub(/\s/, "") != "") and (element.cdatas.size == 0)
      open_tag += element.text
    end
    @output_list.push open_tag
    current_output_lineno = @output_list.size
    # この要素がCDATAセクションを持っているか
    if element.cdatas.size > 0
      cdata_lines = analize_cdatas(element.cdatas)
      cdata_lines.each {|oneline|
        @output_list.push "#{sp}  #{oneline}"
      }
    end
  
    element.each_element { |subelem|
      show_element(subelem, level + 1)
    }
  
    # 出力行数が増えてないなら、開始タグと終了タグの間に挟むべきものがないということ
    if current_output_lineno == @output_list.size
      last_output = @output_list.pop
      if last_output =~ /\>$/
        last_output.gsub!(/\>$/, " />")   # <xp:tag....> → <xp:tag.... />
        @output_list.push last_output
      else
        @output_list.push "#{last_output}</#{element.expanded_name}>"
      end
    else
      @output_list.push "#{sp}</#{element.expanded_name}>"
    end
  end
  
  @output_list = []
  filename = ARGV.shift
  doc = Document.new File.new(filename)
  xml_decl = doc.xml_decl   # XML宣言
  if xml_decl.to_s != ""
    @output_list.push xml_decl.to_s
  end
  show_element(doc.root, 0)
  index = 1
  @output_list.each {|oneline|
    printf("%03d: %s\n", index, oneline)
    index += 1
  }