2008/12/27

End Of 2008年

本年はお世話になりました。来年も変わらぬご愛顧のほど、よろしくお願い申しあげます。

本年お世話になったお客様、誠にありがとうございます。

来年も変わらぬご愛顧のほど、よろしくお願い申しあげます。



さてさて、はやくも2008年が終わろうとしています。

昨年の反省から、情報に埋もれることなく、的確に情報をさばくことを一つの目標としてきまして、相変わらず Google Reaser の未読フィード数は 1000+ ですが、未読を気にせず、Mark all as read する勇気を持つことができました(w)


今年は、Twitter, OpenSocial, Facebook, iPhone, Android と、ますますソーシャルになっていくインターネットですが、この流れはしばらく衰えそうにありませんね。


来年には、docomo, KDDI あたりから、Android 携帯が発表されるらしいので、日本でもすごいことになるのではないでしょうか。


幸い弊社では、iPhone, Android と、実機がそろっていますので、なにか面白いものをつくれるといいなと思います。


そろそろ「破」の段階にきた「守破離」ですが、まだまだ覚えることはたくさんあります。

常に初心を忘れず、さらなる精進を重ねていこうと思う次第です。


守破離とは

「守」とは、


師匠の教えを正確かつ忠実に守り、剣道における基本の作法、礼法、技法を身に付ける、いわば「学び」の段階をいいます。


「破」とは、


それまで身に付けた技や形をさらに洗練させ、自己の個性を創造する段階をいい、


「離」とは、


さらに前進させ、自らの新しい独自の道を確立させる最終段階のことをいいます。自己を常に発展させるべく修行、精進を重ねていく終わりのない道です。



それでは、皆様、よいお年を!

2008/12/26

[Blogger]java.net.ConnectException: Connection timed out

Blogger

Blogger を使っていて、FTP 経由で公開している場合、たまによくわからないエラーに遭遇し、投稿に失敗する場合があります。


その一つが、「java.net.ConnectException: Connection timed out」ですね。


FTPでブログを作成し、初めてのこのエラーが発生した場合は、大概、passive(PASV / パッシブ) FTP が原因です。

Blogger では、アクティブ FTP はサポートされていないため、使用するサーバーでアクティブ FTP を要求していないことを確認します。


しかし、今まで正常に動作していたのに、突然このエラーが出てくることもあります。


そんな時は、次のことをトライしてみるといいかもしれません。



  1. 試しにテストの記事を投稿し、すぐに削除する。その後に再構築をする。

  2. アーカイブのファイルか、atom.xml ファイルをサーバ上から削除し、再構築をする。

  3. しばらく様子を見る


今回僕が遭遇したこのエラーでは、



  • 記事を再投稿し、再構築 ・・・ だめ

  • サーバ上から記事のファイルすべてを削除し、再構築 ・・・ だめ

  • テスト記事を投稿し、削除。再構築 ・・・ だめ

  • しばらく様子を見る ・・・ だめ

  • アーカイブファイルのみを削除し、再構築 ・・・ OK!


という感じでした。


原因は全く不明ですが、同様の問題が発生した場合は参考になれば幸いです。


参考サイト:


2008/12/24

Google Android Dev Phone 1

Android

弊社CEOから、クリスマスプレゼントということで、開発者向けテスト機として Google から販売されている、Android Dev Phone 1 を渡されました。


Android

使い始めるにあたり、次の事項に注意しました。



  1. アクティベーションに、データ接続が必要なので、あらかじめ Mopera U (docomo) を契約

  2. アクティベーション完了後に、Gmail 同期が行われるので、受信トレイをアーカイブしておく


SIMカードは、現在使用中のFOMA携帯用のものでOKでした。


気になるパケット代は、受信トレイをアーカイブしておいたので、mopera U の基本料金300円 + 200円程度のパケット料で済んだようです。


さっそく使ってみると、肝心の WiFi が認識しません!

あれこれ試して、結局、無線LANのチャネルの設定を変更して認識。接続成功です。


使用感は、iPhone に負けないくらいすばらしいですね。

まさに、PC と Mac といった感じでしょうか(良くも悪くも)。

iPhone のマルチタッチは使いやすいですが、Android のシングルタッチ + トラックボールもかなり使いやすいですね。



というわけで、日本で Android 携帯が発売される前には、なにかアプリを作ってみたいですね。

2008/11/30

[Gmail][Greasemonkey]Gmail Template Switcher を Safari, Google Chrome, Opera でも

Firefox, Safari, Chrome, Opera
Gmail Template Switcher
Gmail Template Switch


複数のアカウントで使用したり、差出人によって署名・挨拶文を変えたい場合に非常に便利なGreasemonkey、Gmail Tempolate Switcher ですが、

しばらく前に、Safariでも利用したいとのコメントをいただき、ようやく Greasekit 上の Safari でも動作を確認しました。


Safari 対応に伴い、Greasemetal での Google Chrome と、Opera でも動作を確認しました。


最新版のダウンロードは、こちらから。


Gmail Template Switcher - v 2.0


なお、ご利用方法は、Gmail にテンプレート切り替え機能を付けてみた - 記憶は削除の方向で をご覧ください。

Update:

Google Chrome Extension 版を作成しました。

2008/11/11

[Rails][Plugins] multimodel-forms プラグインがすごい便利

has_many な関連を、ひとつのフォームで登録したいことはよくありますが、そんなときにおすすめなのが、 multimodel-forms プラグインです。



たとえば、ひとつの記事に複数のコメントの場合は、以下のようになります。



まずは、ベースプロジェクトを作成です。



rails sample
cd sample
ruby script/generate scaffold article title:string body:text
ruby script/generate model comment body:text article_id:integer
rake db:migrate


# ---------- app/models/article.rb ----------
class Article < ActiveRecord::Base
has_many :comments
end


# ---------- app/models/comment.rb ----------
class Comment < ActiveRecord::Base
belongs_to :article
end


multimodel-forms プラグインをインストール




ruby script/plugin install git://github.com/sudothinker/multimodel-forms.git


model を修正


# ---------- app/models/article.rb ----------
class Article < ActiveRecord::Base
has_many_with_attributes :comments
end


view を修正


# ---------- app/views/layouts/articles.html.erb ----------
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="content-type" content="text/html;charset=UTF-8" />
<title>Articles: <%= controller.action_name %></title>
<%= stylesheet_link_tag 'scaffold' %>
<%= javascript_include_tag :defaults %>
</head>
<body>

<p style="color: green"><%= flash[:notice] %></p>

<%= yield %>

</body>
</html>


# ---------- app/views/articles/show.html.erb ----------
<p>
<b>Title:</b>
<%=h @article.title %>
</p>

<p>
<b>Body:</b>
<%= simple_format(h(@article.body)) %>
</p>

<h2>Comments</h2>
<ol>
<% @article.comments.each do |c| %>
<li><%= simple_format(h(c.body)) %> <small>(commented <%= time_ago_in_words(c.created_at) %> ago)</small></li>
<% end %>
</ol>

<%= link_to 'Edit', edit_article_path(@article) %> |
<%= link_to 'Back', articles_path %>


# ---------- app/views/articles/new.html.erb ----------
<h1>New article</h1>

<% form_for(@article) do |f| %>
<%= f.error_messages %>

<%= render :partial => 'form', :locals => { :f => f } %>
<p>
<%= f.submit "Create" %>
</p>
<% end %>

<%= link_to 'Back', articles_path %>


# ---------- app/views/articles/edit.html.erb ----------
<h1>Editing article</h1>

<% form_for(@article) do |f| %>
<%= f.error_messages %>

<%= render :partial => 'form', :locals => { :f => f } %>
<p>
<%= f.submit "Update" %>
</p>
<% end %>

<%= link_to 'Show', @article %> |
<%= link_to 'Back', articles_path %>


_form.html.erb を追加


# ---------- app/views/articles/_form.html.erb ----------
<p>
<%= f.label :title %><br />
<%= f.text_field :title %>
</p>
<p>
<%= f.label :body %><br />
<%= f.text_area :body, :rows => 7 %>
</p>

<h3>Comments</h3>
<div id="comments">
<%= render :partial => 'comments/comment', :collection => @article.comments %>
</div>

<%= add_link "Add Comment", :comment %>


comments/_comment.html.erb を追加


# ---------- app/views/comments/_comment.html.erb ----------
<p class="comment">
<% fields_for_associated :article, comment do |comment_form| %>
<%= comment_form.text_area :body, :rows => 2, :index => nil %>
<%= delete_link_for(comment, "Delete", comment_form) %>
<% end %>
</p>


さっそく動作確認です。




非常に便利な、 multimodel-forms プラグインですが、 acts_as_list をサポートしているので、並び替えも簡単にできるようになります。

2008/09/04

Windows で git



Rails の Plugin をインストールしようとしたら、.git な GitHub(ぎっとはぶ)のもので、インストールできませんでした。


GitHub とは

git のホスティングサービス。Rails で作成されており、使いやすいインターフェイスが特徴。


Rails や RSpec 等、また http://gems.github.com/ の Rubygems のレポジトリソース等、Ruby 関係のライブラリのコードを中心とした様々なオープンソースの開発の場所ともなっている。無料で 100M 使えるアカウントを作ることができ、git レポジトリの作成、公開が可能。もちろん Web 上から変更履歴等も参照可能である。


またワンクリックで github で公開されている OSS のコードを fork して開発することが可能で、自分の変更を加えたレポジトリを pull してほしい、等の要望もワンクリックで可能である。


参考:githubとは - はてなキーワード



というわけで、Rails 界では今後 GitHub に移行していくようなので、後ればせながら、git をインストールしました。


git は、 MOONGIFT: » WindowsでGitをはじめるなら「msysGit」:オープンソースを毎日紹介 を参考に、 msysGit をインストール。


これで、 GitHub にあるプラグインもインストールできるようになります。


ruby script/plugin install git://github.com/rubypond/semantic_form_builder.git

2008/08/29

Rails で、アプリケーションから ER図 を生成できる RailRoad を試してみる



Rails には、アプリケーションのモデル・コントローラの内容や関係が記述されたクラス図を、リバースエンジニアリングして生成してくれる RailRoad という便利なツールがあります。


設計は軽くすませて、すぐにプログラミングしていくことが多い Rails アプリケーションですが、全体像を把握したい場合や、他の人に見せたい場合などは、こういうツールがあると便利ですね。


というわけで、実際に使ってみました。


インストール


Graphviz をまずはインストール


railroad をインストール


gem install railroad

Rake タスクとして実行できるようにする


lib/task/diagrams.rake
namespace :doc do
namespace :diagram do
desc "Generate Model diagrams."
task :models do
# SVG
sh "railroad -i -l -a -m -M | dot -Tsvg | sed 's/font-size:14.00/font-size:11.00/g' > doc/models.svg"
# PNG
sh "railroad -i -l -a -m -M | dot -Tpng > doc/models.png"
end

desc "Generate Controller diagrams."
task :controllers do
# SVG
sh "railroad -i -l -C | neato -Tsvg | sed 's/font-size:14.00/font-size:11.00/g' > doc/controllers.svg"
# PNG
sh "railroad -i -l -C | neato -Tpng > doc/controllers.png"
end
end
desc "Generate Model and Controller diagrams."
task :diagrams => %w(diagram:models diagram:controllers)
end

クラス図の生成


rake doc:diagrams

すばらしいですね。

2008/08/28

Rails で migration 時に、Column Comment を設定する

Railsで、マイグレーション作成時に、カラムにコメントを設定し、それをデータベースに設定する ColumnComments という便利なプラグインがあります。

というか、標準では設定できないんですね・・・。


インストールは次の通りです。



  1. こちらより、ZIPファイルをダウンロード

  2. 解凍して column_comments ディレクトリを vendor/plugins へコピー


使い方は、マイグレーション時に次のように記述します。


Example migration:

def self.up
create_table "users" do |t|
t.column "first_name", :string, :comment => "The member's given name."
end

column_comment "tags", "id", "The unique ID of any tag in the system."
end

そしてさらに、テーブルの情報を model と fixture にコメントとして書き込んでくれる annotate_models プラグインもバンドルされています。が、 annotate_models.rb が古いので、こちらは本家のものと置き換えて、コメントを表示するように修正します。


ついでに、 annotate_modelsにindexの情報を付加する - Hello, world! - s21g を参考に、インデックス情報もつけちゃいます。


vendor/plugins/column_comments/lib/annotate_models.rb - self.get_schema_info
  def self.get_schema_info(klass, header)
info = "# #{header}\n#\n"
info << "# Table name: #{klass.table_name}\n#\n"

# index info by http://blog.s21g.com/articles/318
indices = {}
klass.connection.indexes(klass.table_name).each do |index|
index.columns.each do |column_name|
indices[column_name] ||= []
indices[column_name] << "#{index.name}"
indices[column_name].last << "(unique)" if index.unique
end
end

max_size = klass.column_names.collect{|name| name.size}.max + 1
klass.columns.each do |col|
attrs = []
attrs << "default(#{quote(col.default)})" if col.default
attrs << "not null" unless col.null
attrs << "primary key" if col.name == klass.primary_key
if index = indices[col.name]
attrs << index.join(' ')
end

col_type = col.type.to_s
if col_type == "decimal"
col_type << "(#{col.precision}, #{col.scale})"
else
col_type << "(#{col.limit})" if col.limit
end
info << sprintf("# %-#{max_size}.#{max_size}s:%-15.15s %s", col.name, col_type, attrs.join(", ")).rstrip
info << "\n"
# column comment
unless col.comment.blank?
info << "# #{col.comment}"
info << "\n"
end
end

info << "#\n\n"
end

使い方は、次の Rake タスクを実行します。


rake annotate_models

これでもう、テーブルのカラム名を調べるためにデータベースを見る必要はなくなりそうです。

2008/08/27

Rails で Database から Fixture を抽出したい

Rails で、データベースから yaml(やむる) 形式のフィクスチャを抽出するには、ar_fixtures が有名です(手元の Ruby on Rails 逆引きクイックリファレンス Rails 2.0対応 でも紹介されています)。


ですが、実際使ってみると、日本語(UTF8)がうまく表示されなかったり、項目の並びが適当だったりと、あまりよろしくありませんでした。


そこで、データベースからテストフィクスチャを抽出する(to_yaml 不使用) - Rails で行こう! - Ruby on Rails を学ぶ で紹介されている Rake タスクを使うとばっちりうまくいきます。



ar_fixtures は内部的に to_yaml というメソッドを使っていて、これが UTF-8 の文字列をうまく扱えない。そこで、



日本語をto_yamlするとエンコードされてしまう問題を安直な方法で解決する


to_yamlでUTF-8な日本語がbinaryになってしまう問題を回避するRailsプラグイン


みたいな hack が必要になってくる。


そんなわけで、Chad Flower「Rails レシピ」のレシピ41「生データからのテストフィクスチャの抽出」(p155) のソースコードをベースに次のような Rake タスクを作ってみた。to_yaml は使ってないので、UTF-8 文字列にまつわる頭痛とも無縁だ。興味があれば、使ってみてほしい。



参考:データベースからテストフィクスチャを抽出する(to_yaml 不使用) - Rails で行こう! - Ruby on Rails を学ぶ


2008/08/26

Aptana RadRails で test_helper.rb の LoadError が発生する



Edge Rails (2.1.0) と、Aptana RadRails (1.0.3.200807071913NGT)の環境下で、ツールバー上のテストボタンでユニット・テストの実行をすると、以下のエラーが発生します。


c:/ruby/lib/ruby/site_ruby/1.8/rubygems/custom_require.rb:27:in `gem_original_require': no such file to load -- test_helper (LoadError)

コマンドでのテストは問題ないので、「rake test:units」とすればいいのですが、次のようにすれば解決できます。



There is an open issue on the Aptana issue tracker, but it doesn’t seem to be resolved yet.  Some people have suggested prefixing your requires with the test directory when requiring the test_helper file at the start of your test files -but I don’t think that’s a good solution, especially if you’re working with multiple developers.  Frustrated by not getting my daily green bar fix, I found this work-around:


  1. From the Eclipse Preferences option, choose Ruby | Installed Interpreters

  2. Select your interpreter (I use the Standard VM default interpreter named usr) and choose ‘Edit’. 

  3. Add -Itest to the Default VM Arguments option.  Don’t forget the leading dash!


  4. Click ‘OK’. 


参考:Chris Cruft » Blog Archive » Aptana RadRails and the test_helper.rb LoadError


2008/08/18

Rails で、一つのフォームで複数のモデルを扱う

先日のRails講習で、いいことを学んだので忘れないうちにメモです・・。


Rails では、基本、一つの Form に一つの Model なのですが、 fields_for というヘルパーを使用することで複数のモデルを扱えます。


一対一のモデルを一度に更新する場合などに使えそうです。




  <% form_for @person, :url => { :action => "update" } do |person_form| %>
First name: <%= person_form.text_field :first_name %>
Last name : <%= person_form.text_field :last_name %>

<% fields_for @person.permission do |permission_fields| %>

Admin? : <%= permission_fields.check_box :admin %>
<% end %>
<% end %>

参考:Rails Framework Documentation


2008/07/17

Rails 2.1.0 をインストールしてみる

久しぶりに、Railsの環境を更新しようと下記のコマンドこ実行すると、エラーがでました。
やはり、すんなりといきませんねぇ~。


> gem update --system
> gem update rails
Updating installed gems
Updating rails
ERROR: While executing gem ... (Gem::InstallError)
invalid gem format for c:/ruby/lib/ruby/gems/1.8/cache/activesupport-2.1.0.gem

いろいろ調べてみると、activesupportを手動でインストールすることで解決できるようです。



http://rubyforge.org/projects/activesupport/


Download gem to a local file, then


sudo gem install --local activesupport-2.1.0.gem


All fixed.



参考:Riding Rails: Rails 2.1: Time zones, dirty, caching, gem dependencies, caching, etc


2008/07/10

MySQLで行番号

MySQLで行番号を表示したいです。

データベースにMySQLを使っていて、INSERTするときにソート番号を得たいときなど、「SELECT時に行番号を表示したい」ということがたまにあります。


ずいぶん昔にそんなことがあって、その時は、一度Excelにコピペして、行番号をつけて、マクロでSQL文を生成、なんてことをやったのですが、MySQLではユーザ変数をつかうことで行番号を表示することが可能でした。


mysql> set @i=0;
mysql> select (@i:=@i+10) as position, name from users;
+----------+-------+
| position | name |
+----------+-------+
| 10 | hoge1 |
| 20 | hoge2 |
| 30 | hoge3 |
| 40 | hoge4 |
| 50 | hoge5 |
+----------+-------+

変数なんて初めて知りました(汗



ユーザによって定義された変数に値を保存し、後で参照することができます。これで1つのステートメントから次のステートメントに変数を移行させることができます。


MySQL :: MySQL 5.1 リファレンスマニュアル :: 8.4 ユーザによって定義された変数


2008/06/12

Google Developer Day 2008

今年もGoogle Developer Dayに参加してきました。


Google Developer Day 2008 GDD2008


基調講演では、Client / Connectivity / Cloud という「3つのC」が次世代Webのキーワードで、Googleではそれぞれ、Gears / Android / Google App Engine を提供しているというお話でした。


Google Developer Day 2008 GDD2008
Google Developer Day 2008 GDD2008
Google Developer Day 2008 GDD2008

どれもまだ触っていないので、近いうちに弄ってみたいと思います。


午後のセッションでは、今回はコードラボ(その場で実際にコーティングしながらアプリを作る)という新しいタイプのセッションがあったので、最近流行りのOpenSocialのコードラボに参加しました。


Google Developer Day 2008 GDD2008

どうせなら前回とはちょっと違ったものにと軽い気持ちだったのですが、OpenSocialをどのように利用するのかいまいちピンと来ていなかった自分には、他のメンバーとディスカッションすることができて、非常に参考になりました。


作成するアプリのアイディアは次のようなもの



  • 自分と友達で動物占い

  • 友達にタグ付

  • チャット

  • フィードリーダー

  • 地図から友達検索

  • 足跡


いくつかのグループに分かれてアプリを作成するのですが、上記の中でも一番ソーシャルっぽい占いアプリに参加


短い時間で、適当に作業担当を決めて作成していくのですが、みんなでワイワイやるといった感覚は普段はまったくないので、非常に新鮮な体験でした。作り終えたときの感動は格別です。


予習の際、デバッグにひどく苦労したのですが、コードラボではGoogleの方に教えて頂いた、CodeRunnerというアプリのおかげで、すぐさまTry and Errorができ非常に助かりました。


それと、いろいろなAPIが提供されているOpenSocialですが、やはり(APIだけでは)難しいところは、サーバ側で処理するのが常套手段とのことでした。


Google Developer Day 2008 GDD2008

前回とはまた違った刺激を受けることができました。是非また参加したいと思いました。

2008/05/09

Gmail 2.0 で複数の署名を切り替える

GmailGmail Template Switch

僕は普段Gmailを使っていて、Greasemonkeyスクリプトの「Gmail Template Switch」を愛用しています。


複数のアカウントで使用したり、差出人によって署名・挨拶文を変えたい場合に非常に便利なGreasemonkeyスクリプトです。


Gmail Template Switch

大抵皆そうなんだろうけど、メールを書く場合決まった形があって、あいさつ文・本文・締め言葉・署名という順番で書いている。以前作ったスクリプト で、署名は差出人に応じて自動的に切り替わるようになったけど、あいさつ文や締め言葉は辞書に登録したりして、毎回入力してたわけです。会社で使っていることもあり、社内と社外で定型文が変わってくるので、辞書に登録した語句を忘れたりしてかなり不便だった。これはさすがに面倒なので、さらに Gmail を快適にすべく、テンプレートを切り替えられる Greasemonkey スクリプトを作ってみた。これで署名が複数あっても、定型文が複数あっても平気ですな。


Gmail にテンプレート切り替え機能を付けてみた - 記憶は削除の方向で



しかしながら、Gmail Template Switch は、Gmail 2.0では動作しないため、動作が快速でラベルの色分け等便利な機能は我慢し、日本語版Gmailを使っていたわけですが、先日ついに、日本語版Gmailも 2.0になってしまいました。


というわけで、Gmail Template Switch を、Gmail 2.0 で動作するようにハックしてみました。


Gmail Template Switcher - v 2.0


既存のデータは生かしたかったので、コードはほとんど流用しています。なお、jQueryの使用で意味不明なエラーが出たので、使わないようにハックしてます。

また、Gmail 2.0は、Greasemonkeyを正式にサポートするらしく、GmailGreasemonkey10APIが公開されているので、せっかくなので使ってみました。


はじめは軽い気持ちでやってみたのですが、Greasemonkeyは初めて、かつ、Gmail 2.0 になってずいぶん変わっているようで、とても苦戦しました・・・(汗


とくに、ElementのIDがランダムで変わるのと、Reply時の入力フォームが動的に挿入される点でかなりはまりました・・。


なお、操作方法等はオリジナルとかわりません。もちろん新しい機能なんてありませんw


以下、コード全文です。


※最新のコードはこちら


Update:

Safari (Greasekit), Google Chrome, Opera で動作を確認しました。

Update2:

Google Chrome Extension 版を作成しました。


gmailtemplateswitcherv20.user.js
// ==UserScript==
// @name Gmail Template Switcher - v 2.0
// @namespace http://www.r-stone.net/blogs/ishikawa
// @description Append the function to apply the mail template, when writing a mail. Modify source code of [Gmail Template Switch] from http://d.hatena.ne.jp/re_guzy by re_guzy
// @version 0.1.20080510.0
// @include http://mail.google.com/*
// @include https://mail.google.com/*
// @exclude http://mail.google.com/mail/help/*
// @exclude https://mail.google.com/mail/help/*
//
// Copyright (c) 2007-2008, re_guzy <goodspeed.xii@gmail.com>
// Distributed under the MIT license
// http://opensource.org/licenses/mit-license.php
// http://sourceforge.jp/projects/opensource/wiki/licenses%2FMIT_license
//
// Notice : To Uninstall this script, remove "gtssettings0-9" from Gmail contact list.
// Feature: When writing a mail, append a combobox to action. By selecting action,
// apply template to mail, or add template or remove template. Template
// is saved to "contact list" named starting with "gtssettings".
// Require: Greasemonkey 0.7.20080121.0
// ==/UserScript==

const DEBUG = false;
const KEY_TOKEN = "gts_token";
const KEY_CACHE = "gts_cache";
const CONTACT_NAME = "gtssettings";
const CONTACT_ID_RE = /\["\w+","(\w+)","gtssettings\d","gtssettings\d",/;
const MSGBODY_RE = /([\s\S]*)\n?(?:^---)(\n[\s\S]+)/m;
const LOCATION_RE = /(https?:\/\/[^\/]+\/(a\/[^\/]+\/)?).*/;
const SELECTOR = {
'gts' : 'descendant::*[local-name() = "select" or local-name() = "SELECT"][@id = "id_gts_template"]',
'input_form' : 'descendant::*[local-name() = "form" or local-name() = "FORM"]',
'body' : 'descendant::*[local-name() = "textarea" or local-name() = "TEXTAREA"][@name = "body"]',
'subject' : 'descendant::*[local-name() = "input" or local-name() = "INPUT"][@name = "subject"]',
'to' : 'descendant::*[local-name() = "textarea" or local-name() = "TEXTAREA"][@name = "to"]',
'from' : 'descendant::*[local-name() = "select" or local-name() = "SELECT"][@name = "from"] | descendant::*[local-name() = "input" or local-name() = "INPUT"][@name = "from"]',
'cc' : 'descendant::*[local-name() = "textarea" or local-name() = "TEXTAREA"][@name = "cc"]',
'bcc' : 'descendant::*[local-name() = "textarea" or local-name() = "TEXTAREA"][@name = "bcc"]',
'gts_undo' : 'descendant::*[contains(concat(" ",@class," "), " gts_undo_option ")]',
'gts_first' : 'descendant::*[contains(concat(" ",@class," "), " gts_option_first ")]',
'discard' : 'descendant::*[local-name() = "button" or local-name() = "BUTTON"][count(preceding-sibling::*[local-name() = "button" or local-name() = "BUTTON"]) = 2]',
'labels' : 'descendant::*[local-name() = "span" or local-name() = "SPAN"]',
'msg' : 'div/div/div/div/div/div/div/div/div/div/div[3]/div/div[2]/div[2]/div/div[2]/div[2]/div/table/tbody/tr[2]/td[2]'
}

var T = new Array(10);
T[0] = { 'id' : -1, 'num' : 0 };
for (var i=1;i < T.length;i++) {
T[i] = {
'id' : -1,
'num' : i,
'from' : '',
'to' : '',
'cc' : '',
'bcc' : '',
'subject' : '',
'body' : '',
'body_latter' : ''
}
}
var Ja = false;
var recentView;

//Initialize gmail and gmonkey objects
window.addEventListener('load', function() {
if (unsafeWindow.gmonkey) {
unsafeWindow.gmonkey.load('1.0', function(gmail) {

function getViewType() {
var str = '';
switch (gmail.getActiveViewType()) {
case 'tl': str = 'Threadlist'; break;
case 'cv': str = 'Conversation'; break;
case 'co': str = 'Compose'; break;
case 'ct': str = 'Contacts'; break;
case 's': str = 'Settings'; break;
default: str = 'Unknown';
}
return str;
}

function getView() {
return gmail.getActiveViewElement();
}

function getMailForm() {
var a = xpath(SELECTOR['input_form'], getView());
if (a && a.length > 0) {
return a[0];
} else {
return null;
}
}

function getGts() {
var a = xpath(SELECTOR['gts'], getView());
if (a && a.length > 0) {
return a[0];
} else {
return null;
}
}

function getGtsOptUndos() {
return a = xpath(SELECTOR['gts_undo'], getGts());
}

function getGtsOptFirst() {
var a = xpath(SELECTOR['gts_first'], getGts());
if (a && a.length > 0) {
return a[0];
} else {
return null;
}
}

function getFrom(form) {
var a = xpath(SELECTOR['from'], form || getMailForm());
if (a && a.length > 0) {
return a[0];
} else {
return null;
}
}

function getTo(form) {
var a = xpath(SELECTOR['to'], form || getMailForm());
if (a && a.length > 0) {
return a[0];
} else {
return null;
}
}

function getCc(form) {
var a = xpath(SELECTOR['cc'], form || getMailForm());
if (a && a.length > 0) {
return a[0];
} else {
return null;
}
}

function getBcc(form) {
var a = xpath(SELECTOR['bcc'], form || getMailForm());
if (a && a.length > 0) {
return a[0];
} else {
return null;
}
}

function getSubject(form) {
var a = xpath(SELECTOR['subject'], form || getMailForm());
if (a && a.length > 0) {
return a[0];
} else {
return null;
}
}

function getBody(form) {
var a = xpath(SELECTOR['body'], form || getMailForm());
if (a && a.length > 0) {
return a[0];
} else {
return null;
}
}

function getDiscard() {
var a = xpath(SELECTOR['discard'], getMailForm().parentNode.parentNode);
if (a && a.length > 0) {
return a[0];
} else {
return null;
}
}

function getLabels(form) {
return a = xpath(SELECTOR['labels'], form || getMailForm());
}

function getCcLabel() {
var a = getLabels();
for (var i=0; i<a.length; i++) {
if (/Cc/.exec(a[i].innerHTML)) {
return a[i];
}
if (i>1) {
break;
}
}
return null;
}

function getBccLabel() {
var a = getLabels();
for (var i=0; i<a.length; i++) {
if (/Bcc/.exec(a[i].innerHTML)) {
return a[i];
}
if (i>1) {
break;
}
}
return null;
}

function getChangeLabel() {
var a = getLabels();
for (var i=0; i<a.length; i++) {
if (/change/.exec(a[i].innerHTML) || /変更/.exec(a[i].innerHTML)) {
return a[i];
}
if (i>1) {
break;
}
}
return null;
}

function switcher() {
str = getViewType();

if (str != "Compose" && str != "Conversation") {
if (recentView) {
recentView.removeEventListener('DOMNodeInserted', nodeInsertedHandler, false);
}
return;
}
recentView = getView();
window.setTimeout(function() {
initialize();
}, 600);
}

function nodeInsertedHandler(event) {
target = event.target;
if (target.nodeType == 1) {
tagName = target.tagName.toLowerCase();
if (tagName == 'form') {
log('form inserted');
window.setTimeout(function() {
initialize();
}, 500);
//getView().removeEventListener('DOMNodeInserted', nodeInsertedHandler, false);
} else if (tagName == 'table') {
log('table inserted');
window.setTimeout(function() {
initialize();
}, 500);
//getView().removeEventListener('DOMNodeInserted', nodeInsertedHandler, false);
}
}
}

function initialize() {
log('initialize');

try {

if (getGts()) {
log('already initialized');
return;
}

var form = getMailForm();
if (!form) {
log('form not found');
getView().addEventListener('DOMNodeInserted', nodeInsertedHandler, false);
return;
}
//getView().removeEventListener('DOMNodeInserted', nodeInsertedHandler, false);

var discard_button = getDiscard();
var label = discard_button.innerHTML;
Ja = (label == '破棄');
discard_button.parentNode.insertBefore(createSelectElement(), discard_button.nextSibling);
composeCommand(form);
} catch(e) {
log('add combobox failure because : ' + e);
return;
}
}

function createSelectElement() {
var content = document.createElement('select');
content.setAttribute("id", "id_gts_template");
content.setAttribute("style", "margin-left:10px;font-size:.8em;");
content.innerHTML = toOption('please wait...' , false , true);
content.addEventListener('change', function(event) {doCommand(event.target)}, true);
return content
}

function toOption(text, value, selected, cls, isDom) {
if (isDom) {
var attr = {
'style' : value ? null : 'color: rgb(119, 119, 119);',
'disabled' : value ? null : 'disabled',
'selected' : selected ? 'selected' : null,
'value' : value ? value : null,
'class' : cls ? cls : null
};
var elm = document.createElement('option');
for (var i in attr) {
if (attr[i]) {
elm.setAttribute(i, attr[i]);
}
}
elm.innerHTML = text;
return elm;
} else {
var attr = {
'style' : value ? null : '"color: rgb(119, 119, 119);"',
'disabled' : value ? null : '"disabled"',
'selected' : selected ? '"selected"' : null,
'value' : value ? '"' + value + '"' : null,
'class' : cls ? cls : null
};
var a = [];
for (var i in attr) {
if (attr[i]) {
a.push(i + "=" + attr[i]);
}
}
return "<option " + a.join(' ') + ">" + text + "</option>";
}
}

function composeCommand(form) {
getTemplates(function /*parseTemplate*/(notes, use_cache) {
for (var i in notes) {
if (use_cache) {
T[i] = notes[i];
} else {
var note = notes[i].note ? decode(notes[i].note, false) : "{}";
try {
T[notes[i].num] = eval(note);
T[notes[i].num].id = notes[i].id;
T[notes[i].num] = decode(T[notes[i].num], true);
} catch(e) {log("eval failed : " + e);}
}
}

recomposeSelectElement(form);
applyDefault(form);
if (notes.length == 0) {
save();
} else if (!use_cache) {
var caches = [];
for (var i in T) {
var encoded = encode(T[i], true);
if (encoded) {
caches.push(encoded.toSource());
}
}
GM_setValue(KEY_CACHE, "[" + caches.join(", ") + "]");
}
});
}

function applyDefault(form) {
var from = getFrom();
if (from) {
var fromvalue = from.value;
matched = grep(T, function(i) {
return (i.name + '').indexOf('#') == 0 && i.from == fromvalue;
});
if (matched.length > 0) {
applyTemplate(matched[0].num, form);
}
}
}

function recomposeSelectElement(form) {
var options = [];
options.push(toOption(trans("Template actions...") , "init" , true, "gts_option_first"));

var enables = grep(T, function(o) {return (o.name && o.num != 0);});
var expand = function(arrays, cmd) {
var hash = {};
for (var i in enables) {
hash[enables[i].from] = hash[enables[i].from] || [];
hash[enables[i].from].push(enables[i]);
}
for (var i in hash) {
arrays.push(toOption(' <' + (i || trans('No from')) + '>'));
for (var j in hash[i]) {
arrays.push(toOption('  ' + hash[i][j].name , cmd + '_' + hash[i][j].num));
}
}
};

var tmp = [
{'cmd':'apply', 'exp':trans("Apply"), 'func':expand},
{'cmd':'add','exp':trans("Append"), 'func':function(arrays, cmd) {
var used = grep(T , function(o) { return (o.name && o.num != 0) });
if (used.length < 9) {
arrays.push(toOption('  ' + trans("Includes from") , cmd));
arrays.push(toOption('  ' + trans("Excludes from") , cmd + '_ignore_from'));
} else {
arrays.push(toOption('  ' + trans('Quantity limit is 9')));
}
}},
{'cmd':'delete','exp':trans('Remove'), 'func':expand}
];
for (var i in tmp) {
if (tmp[i].func != expand || enables.length != 0) {
options.push(toOption('-------'));
options.push(toOption(trans('verbs', tmp[i].exp) + ':'));
tmp[i].func(options, tmp[i].cmd);
}
}

var gts = getGts();
gts.innerHTML = options.join('');
gts.value = 'init';
}

function doCommand(selectNode) {
var form = getMailForm();
if (form) {
if (selectNode.value == 'add') {
addTemplate(form, true);
} else if (selectNode.value == 'add_ignore_from') {
addTemplate(form, false);
} else if (selectNode.value.match(/apply_(\d+)/)) {
applyTemplate(RegExp.$1 , form);
} else if (selectNode.value.match(/delete_(\d+)/)) {
deleteTemplate(RegExp.$1 , form);
} else if (selectNode.value == 'undo') {
if (unsafeWindow.gts_undo) { unsafeWindow.gts_undo(); }
}
selectNode.value= 'init';
} else {
log('form not found');
}
}

function addTemplate(form, contain_from) {
var m = trans('Please input the template name.');
if (contain_from) {
m += '\n';
m += trans('If the name is started from "#", it becomes default of the corresponding "from".');
}
var name = window.prompt(m, "");
if (!name) {return;}
if (grep(T , function(o) { return (o.name == name) }).length > 0) {
alert(trans('The name already exists.'));
return;
}

var empties = grep(T , function(o) { return (o.name || o.num == 0) }, true);
var t = empties[0];

var to = getTo(form);
var b = getBody(form);
var c = getCc(form);
var bc = getBcc(form);
var f = getFrom(form);
var s = getSubject(form);
T[t.num] = {
'num' : t.num,
'id' : t.id,
'name' : name,
'from' : contain_from ? f.options[f.selectedIndex].value : "",
'to' : to.innerHTML,
'cc' : c.innerHTML,
'bcc' : bc.innerHTML,
'subject' : s.value,
'body' : b.innerHTML
};

if (MSGBODY_RE.exec(T[t.num].body)) {
T[t.num].body = RegExp.$1;
T[t.num].body_latter = RegExp.$2;
}

editContact(T[t.num], function() {
recomposeSelectElement(form);
msg(trans('appended', name));
});
}

function applyTemplate(num , form) {
if (typeof T[0]._init == 'undefined') {
T[0]._init = {};
var selectors = x('f,s,t,c,bc,bo');
for (var i in selectors) {
var j = xpath(selectors[i], form)[0];
T[0]._init[j.name] = trim(j.value);
}
}

var selectors = x('f,s,t,c,bc');
for (var i in selectors) {
var j = xpath(selectors[i], form)[0];
var tmp = T[0]._init[j.name];
if (j.name != 'from' || j.value != T[num][j.name]) {
if (j.type == 'textarea') {
var targetaddrs = tmp || "";
//既に含まれているものは追加しない
var notcontains = grep(T[num][j.name].split(','), function(i) {
if (trim(i).length == 0) { return; }//空白は無視
if (/([\w\.+-]+@[\w+-]+(\.[\w+-]+)+)/.exec(i)) {
return targetaddrs.indexOf(RegExp.$1) < 0;
} else {
return targetaddrs.indexOf(i) < 0;
}
});
if (tmp) { notcontains.unshift(tmp); }
j.value = notcontains.join(', ');
} else {
j.value = (j.name == 'subject' && tmp) ? tmp : T[num][j.name];
}
if (j.name == 'cc' && T[num][j.name]) {
var cc_label = getCcLabel();
if (cc_label) {
emulate_click(cc_label);
}
}
if (j.name == 'bcc' && T[num][j.name]) {
var bcc_label = getBccLabel();
if (bcc_label) {
emulate_click(bcc_label);
}
}
}
}

var change_label = getChangeLabel();
if (change_label) {
emulate_click(change_label);
}

var b = getBody(form);
b.value = grep([
T[num].body, T[0]._init.body, T[num].body_latter
], function(i) { return i; }).join("\n\n");

var selectors = x('bo,s,t');
for (var i in selectors) {
var j = xpath(selectors[i], form)[0];
if ((j.name == 'body') || !j.value) {
j.focus();
j.selectionStart = 0;
j.selectionEnd = 0;
}
}

var undos = [
toOption('-------', null, null, "gts_undo_option", true),
toOption('  ' + trans("Undo"), 'undo', null, "gts_undo_option", true)
];
var gts_undos = getGtsOptUndos();
for (var i=0; i<gts_undos.length; i++) {
gts_undos[i].parentNode.removeChild(gts_undos[i]);
}
var gts_first = getGtsOptFirst();
if (gts_first) {
gts_first.parentNode.insertBefore( undos[1], gts_first.nextSibling );
gts_first.parentNode.insertBefore( undos[0], gts_first.nextSibling );
}

msg(trans('applied', T[num].name), function() {
undo(form);
msg(trans("To apply template was canceled."));
}, true);
}

function undo(form) {
if (typeof T[0]._init != 'undefined') {
var selectors = x('f,t,c,bc,bo');
for (var i in selectors) {
var j = xpath(selectors[i], form)[0];
j.value = T[0]._init[j.name];
}
}
delete T[0]._init;
var gts_undos = getGtsOptUndos();
for (var i=0; i<gts_undos.length; i++) {
gts_undos[i].parentNode.removeChild(gts_undos[i]);
}
}

function emulate_click(target) {
if (target.dispatchEvent) {
var e = unsafeWindow.document.createEvent("MouseEvents");
e.initEvent("click", true, true);
target.dispatchEvent(e);
}
}

function deleteTemplate(num , form) {
var name = T[num].name;
if (confirm(trans('remove confirm', name)) != true) {
return;
}

T[num] = {'id' : T[num].id, 'num' : num};
editContact(T[num], function() {
recomposeSelectElement(form);
msg(trans("removed", name));
});
}

function getTemplates(f_parseTemplate) {
var queryUrl = 'mail/contacts/data/contacts?thumb=false&groups=false&show=ALL&psort=Name&max=300&out=js&rf=&jsx=true';
ajax(queryUrl, function(req){
contactPage = req.responseText.replace('while (true); ', '').replace(/&&&START&&&([^&&&]+)&&&END&&&/, "$1");
response = eval("(" + contactPage + ")");
if (response.Success) {
var contacts = response.Body.Contacts;
var notes = [];
for(i=0; i<contacts.length; i++) {
if (contacts[i].Name && /gtssettings(\d)/.exec(contacts[i].Name) ) {
num = RegExp.$1;
note = contacts[i].Notes;
id = contacts[i].Id;
notes.push({'num' : num, 'note' : note, 'id' : id});
}
}
var authtoken = response.Body.AuthToken.Value;
GM_setValue(KEY_TOKEN, authtoken);
f_parseTemplate(notes);
} else {
log("Contacts Request Failed: " + response.Errors[0].Text);
}
});
}

function encode(tmpl, by_escape) {
if (by_escape) {
var escaped = {};
//encodeURIだと「'」がエンコードされないので、escapeを使う
for (var i in tmpl) {
if (i.indexOf('_') != 0) {
escaped[i] = escape(tmpl[i]);
}
}
return escaped;
} else {
//連絡先は「"」で囲まれるため、JSONデータを表すのに「"」を使えない。
//連絡先から復元するときに使うデータを、REGEXでマッチさせるため「"」を「'」に置換しておく。
return tmpl.toSource().replace(/\"/g, "'");
}
}

function decode(tmpl, by_unescape) {
if (by_unescape) {
var unescaped = {};
for (var i in tmpl) {
unescaped[i] = unescape(tmpl[i]);
}
return unescaped;
} else {
//連絡先に格納するために、「'」に変換しておいた「"」を戻す
return tmpl.replace(/\\'/g, "\"");
}
}

function editContact(tmpl, f_completed) {
var authtoken = GM_getValue(KEY_TOKEN);
if (!authtoken) {
log('token not found');
return;
}
var escaped = encode(tmpl, true);
var post_data = param({
"token" : authtoken,
"tok" : authtoken,
"out" : "js",
"id" : tmpl.id,
"action" : "SET",
"Name" : CONTACT_NAME + tmpl.num,
"Emails.0.Address" : CONTACT_NAME + tmpl.num + "@gmail.com",
"Notes" : encode(escaped, false)
});
ajax("mail/contacts/update/contact", function(req) {
var response = eval("(" + req.responseText.replace('while (true); ', '').replace(/&&&START&&&([^&&&]+)&&&END&&&/, "$1") + ")");
if (response.Success) {
if (tmpl.id == -1) {
if (CONTACT_ID_RE.exec(req.responseText)) {
tmpl.id = RegExp.$1;
editContact(tmpl, f_completed);
}
} else {
if (tmpl.num != 0) {
save(f_completed);
} else {
f_completed();
}
}
} else {
log("Update Contact Request Failed: " + response.Errors[0].Text);
}
}, 'POST', {'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8'}, post_data);

}

function log(message) {
if (unsafeWindow && unsafeWindow.console && DEBUG) {
unsafeWindow.console.log(message);
}
}

function getCookie(name) {
var re = new RegExp(name + "=([^;]+)");
var value = re.exec(document.cookie);
return (value != null) ? decodeURI(value[1]) : null;
}

function msg(message, f_clicked, is_undo) {
unsafeWindow.gts_undo = f_clicked;
var a = xpath(SELECTOR['msg'], getView().ownerDocument.body);
if (a && a.length > 0) {
var td = a[0];
var div = td.parentNode.parentNode.parentNode.parentNode;
div.style.visibility = "visible";
td.innerHTML = "GTS : " + message;
if (is_undo) {
var span = document.createElement('span');
span.setAttribute("id", "gts_und");
span.setAttribute("class", "lk");
span.innerHTML = trans('Undo link');
span.addEventListener('click', f_clicked, true);
td.appendChild(span);
}
window.setTimeout(function() {
div.style.visibility = "hidden";
}, 60000);
}
}

function save(f_saved) {
T[0]['num'] = 0;
T[0].date = new Date();
editContact(T[0], f_saved ? f_saved : function() {});
}

function ajax(request_path, f_load, get_or_post, headers, data) {
window.setTimeout(function() {
GM_xmlhttpRequest({
'method': get_or_post ? get_or_post : "GET",
'url': getBaseLocation() + request_path,
'data': data,
'headers': headers,
'onload': f_load,
'onerror': function(req) {
log("Request Failed in error code: " + req.status);
}
});
}, 0);
}

function getBaseLocation() {
if (LOCATION_RE.exec(document.location)) {//for Google Apps
return RegExp.$1;
} else {
return 'http://mail.google.com/';
}
}

function trans(msg_id, opt) {
return {
'Template actions...' : Ja ? 'テンプレートの操作...' : msg_id,
'Apply' : Ja ? '適用' : msg_id,
'Append' : Ja ? '追加' : msg_id,
'Includes from' : Ja ? '差出人を含む' : msg_id,
'Excludes from' : Ja ? '差出人を除く' : msg_id,
'Quantity limit is 9' : Ja ? '最大9個です' : msg_id,
'Remove' : Ja ? '削除' : msg_id,
'verbs' : Ja ? (opt + 'するテンプレート') : (opt + ' template'),
'Please input the template name.' : Ja ? 'テンプレート名を入力してください。' : msg_id,
'If the name is started from "#", it becomes default of the corresponding "from".' :
Ja ? '名前を「#」から始めると、対応する差出人のデフォルトになります。' : msg_id,
'The name already exists.' : Ja ? 'その名前は既に存在します。' : msg_id,
'appended' : Ja ? ("テンプレート「" + opt + "」を追加しました。")
: ('Template "' + opt + '" was appended.'),
'applied' : Ja ? ("テンプレート「"+opt+"」を適用しました。")
: ('Template "' + opt + '" was applied. '),
'remove confirm' : Ja ? ("テンプレート「" + opt + "」を削除しますか?")
: ('Is template "' + opt + '" removed?'),
'removed' : Ja ? ("テンプレート「" + opt + "」を削除しました。")
: ('Template "' + opt + '" was removed.'),
'To apply template was canceled.' : Ja ? "テンプレートの適用は取り消されました。" : msg_id,
'Undo' : Ja ? "適用の取り消し" : msg_id,
'Undo link' : Ja ? "適用取り消し" : "Undo applied",
'No from' : Ja ? "差出人なし" : msg_id
}[msg_id] || msg_id;
}

function x(prefix) {
var result = [];

for (var i in SELECTOR) {
if (typeof prefix != 'undefined') {
var a = grep(prefix.split(','), function(j) { return i.indexOf(j) == 0; });
if (a.length != 0) { result.push(SELECTOR[i]); }
} else {
result.push(SELECTOR[i]);
}
}
return result;
}

/*
* this 'grep' function from jquery-1.2.2.js
* jQuery 1.2.2 - New Wave Javascript
*/
function grep( elems, callback, inv ) {
// If a string is passed in for the function, make a function
// for it (a handy shortcut)
if ( typeof callback == "string" )
callback = eval("false||function(a,i){return " + callback + "}");

var ret = [];

// Go through the array, only saving the items
// that pass the validator function
for ( var i = 0, length = elems.length; i < length; i++ )
if ( !inv && callback( elems[ i ], i ) || inv && !callback( elems[ i ], i ) )
ret.push( elems[ i ] );

return ret;
}

/*
* this 'param' function from jquery-1.2.2.js
* jQuery 1.2.2 - New Wave Javascript
*/
function param(a) {
var s = [];

// Serialize the key/values
for ( var j in a )
// If the value is an array then the key names need to be repeated
s.push( encodeURIComponent(j) + "=" + encodeURIComponent( a[j] ) );

// Return the resulting serialization
return s.join("&").replace(/%20/g, "+");
}

/*
* this 'trim' function from jquery-1.2.2.js
* jQuery 1.2.2 - New Wave Javascript
*/
function trim(text) {
return (text || "").replace( /^\s+|\s+$/g, "" );
}

function xpath(query, target) {
var results = document.evaluate(query, target || document, null,
XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
var nodes = [];
for (var i=0; i<results.snapshotLength; i++) {
nodes.push(results.snapshotItem(i));
}
return nodes;
}

gmail.registerViewChangeCallback(switcher);
switcher();
});
}
}, true);

2008/04/25

ホスト名とOpenID

Rails で OpenID を試してみようと思って思わぬところで躓いたのでメモです。


ホスト名に使用できる文字は、英数字文字(a-zA-Z0-9)および、ハイフン(-)となっています。


ホスト名について

ホスト名(hostname)は,[RFC1034]の3.及び[RFC1123]の2.1 で示される形式をとる。すなわち,"."によって分離されたドメインラベルの列であって,各々のドメインラベルは,英数字文字(alphanum)で開始及び終了し,"-"文字を含んでもよい。完全限定ドメイン名の最も右にあるドメインラベルは,数字で始まってはならない。そのために,IPv4アドレスとは構文的に区別されるドメイン名になる。


Uniform Resource Identifiers (URI): Generic Syntax: Main(日本語)



テストのために自分のOpenIDを取得したのですが、これがまた、上で述べたルールに反したものを取得してしまいました・・。


取得したIDは、「ishikawa_rs.openid.ne.jp」なのですが、アンダーバーが入っています><


このIDをOpenIDのプラグインであるopen_id_authenticationに通すと、「ishikawa_rs.openid.ne.jp is not an OpenID URL」と言われてしまいます。


エラーが発生するところをirbで再現してみると下記のようになります。


irb(main):001:0> require 'uri'
=> true
irb(main):002:0> url = "ishikawa_rs.openid.ne.jp"
=> "ishikawa_rs.openid.ne.jp"
irb(main):003:0> uri = URI.parse(url.to_s.strip)
=> #
irb(main):004:0> uri = URI.parse("http://#{uri}") unless uri.scheme
URI::InvalidURIError: the scheme http does not accept registry part: ishikawa_rs
.openid.ne.jp (or bad hostname?)
from c:/ruby/lib/ruby/1.8/uri/generic.rb:195:in `initialize'
from c:/ruby/lib/ruby/1.8/uri/http.rb:78:in `initialize'
from c:/ruby/lib/ruby/1.8/uri/common.rb:488:in `new'
from c:/ruby/lib/ruby/1.8/uri/common.rb:488:in `parse'
from (irb):4
irb(main):005:0>

bad hostnameです。


そもそも、なぜこのIDになったかというと、「ishikawa.rs」にしようとしたところ、「ドット(.)はだめです。アンダーバー(_)は使えるよ。」と言われたからでした・・。



OpenIDを取得する際、ユーザIDがホスト名になる場合はご注意ください。


※そして、困ったことにopenid.ne.jpではアカウントの削除ができないようです。

2008/03/28

Rails と AIR で、付箋紙アプリ

Stickynotes

Rails の最新は 2.0.2 ですし、AIR は正式版の 1.0 が公開されました。

情報はある程度追ってはいるものの、やはり実際に試してみないとなかなか身につきません。


というわけで、以前から気になっていた、[Think IT] 第1回:付箋紙アプリケーションを作ろう!を参考に、Ruby on RailsとAIRによるデスクトップ付箋紙アプリケーションを作ってみました。


Adobe AIR のインストールはこちらから。


サンプル付箋紙アプリをお試しいただく場合は、こちらから。

ちなみに、このアプリはユーザ管理はしておりません。ので、大変ソーシャルな付箋アプリです(汗


Download Stickynotes AIR


基本的な動作は、[Think IT] 第1回:付箋紙アプリケーションを作ろう!と、Ruby on RailsとAdobe AIRでデスクトップアプリを作る - Pokeal.COMをベースに、なんとなくタスクトレイアイコンも使ってみました。ついでに、常に前面表示も可能です。


Railsに関しては、実質、次の2行のみです。これでRestfulなバックエンドアプリのできあがりです・・。


 $ ruby script/generate scaffold sticky body:text x:float y:float width:integer height:integer
$ rake db:migrate

なお、Railsアプリは、先日紹介した、Herokuで構築しています。


AIRでは、透過するTextFieldを作るのにちょっと苦労しました。
ポイントは、blendModeをLAYERにしたSpriteでした。


dynamic textfield alpha problem - kirupaForum

I am not sure if this is quite what you are looking for but here is a solution I found for adjusting alpha on a TextField object.


// create an empty sprite
var rect:Sprite = new Sprite();
rect.blendMode = BlendMode.LAYER;

addChild(rect);

var txtField:TextField = new TextField();
txtField.txtColor = 0x000000;
txtField.alpha = .5;
txtField.appendText("SOME TEXT");

rect.addChild(txtField);

the key is to set the blendMode property on the sprite to LAYER.



以下、参考までにソースコードです。


Menu.mxml


<?xml version="1.0" encoding="utf-8"?>
<mx:WindowedApplication xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute" width="300" height="200" creationComplete="onCreationComplete();" closing="closing(event);">
<mx:Script>
<![CDATA[
import mx.core.BitmapAsset;

[Embed(source="icons/broken-16x16.png")]
private var icon16:Class;

private var stickies:Array = [];

private function create():void {
setStatus(">> Create new sticky");

var sticky:Sticky = new Sticky();

sticky.setAlwaysInFront(isAlwaysInFront.selected);
sticky.save();
sticky.show();
stickies.push(sticky);

}

private function onCreationComplete():void {
setStatus(">> Initializing...");

// is supports system tray icon
if (NativeApplication.supportsSystemTrayIcon) {

var images:Array = [];
images.push((new icon16() as BitmapAsset).bitmapData);
nativeApplication.icon.bitmaps = images;

var systemTrayIcon:SystemTrayIcon = (nativeApplication.icon as SystemTrayIcon);

systemTrayIcon.tooltip = "Stickynotes";

var nativeMenu:NativeMenu = new NativeMenu();
var menuItemNew:NativeMenuItem = new NativeMenuItem("New Stickynote");
menuItemNew.addEventListener(Event.SELECT, function(e:Event):void {
create();
});
var menuItemReload:NativeMenuItem = new NativeMenuItem("Reload Stickynotes");
menuItemReload.addEventListener(Event.SELECT, function(e:Event):void {
load();
});
var menuItemRestore:NativeMenuItem = new NativeMenuItem("Restore window");
menuItemRestore.addEventListener(Event.SELECT, function(e:Event):void {
restore();
});
var menuItemExit:NativeMenuItem = new NativeMenuItem("Exit");
menuItemExit.addEventListener(Event.SELECT, function(e:Event):void {
exit();
});
nativeMenu.addItem(menuItemNew);
nativeMenu.addItem(menuItemReload);
nativeMenu.addItem(menuItemRestore);
nativeMenu.addItem(menuItemExit);
systemTrayIcon.menu = nativeMenu;

}

load();
}

private function load():void {
setStatus(">> Loading from http://stickynotes.heroku.com ...");

closing(null);

var request:URLRequest = new URLRequest("http://stickynotes.heroku.com/stickies.xml");
request.method = 'GET';
var loader:URLLoader = new URLLoader();
loader.addEventListener(Event.COMPLETE, function(e:Event):void {
messages.text += e.target.data + "\n";

var xml:XML = new XML(e.target.data);
for each (var element:Object in xml.sticky) {
var sticky:Sticky = new Sticky();
sticky.id = element.id;
sticky.editor.text = element.body;
sticky.window.x = element.x * Capabilities.screenResolutionX;
sticky.window.y = element.y * Capabilities.screenResolutionY;
sticky.window.width = element.width;
sticky.window.height = element.height;
sticky.setAlwaysInFront(isAlwaysInFront.selected);
sticky.updateStatus();
sticky.show();

stickies.push(sticky);
}

clearStatus();
});
loader.load(request);
}

private function setStatus(s:String):void {
this.status = s;
}
private function clearStatus():void {
this.status = "";
}

private function closing(e:Event):void {
for each (var sticky:Sticky in stickies) {
if (sticky) {
sticky.window.close();
}
}
stickies = [];
}

private function saveAll():void {
setStatus(">> Save stickies");

for each(var sticky:Sticky in stickies) {
sticky.save();
}
}

private function onChangeHandle():void {
for each (var sticky:Sticky in stickies) {
sticky.setAlwaysInFront(isAlwaysInFront.selected);
}
}

]]>
</mx:Script>
<mx:Button left="10" top="10" label="New" id="new_btn" click="create();" />
<mx:Button left="60" top="10" label="Reload" id="load_btn" click="load();" />
<mx:Button right="10" top="10" label="Save" id="save_btn" click="saveAll();" />
<mx:CheckBox left="10" top="40" label="always in front" id="isAlwaysInFront" change="onChangeHandle();" />
<mx:TextArea right="10" top="70" left="10" bottom="10" id="messages" />
</mx:WindowedApplication>


Sticky.as


package {
import flash.text.*;
import flash.display.*;
import flash.events.*;
import flash.system.*;
import flash.net.*;
import mx.controls.*;

public class Sticky {
public var window:NativeWindow; // sticky window
public var editor:TextField; // sticky edit area
public var id:Number; // primary key
private var button:SimpleButton = new SimpleButton(); // cloase button
private var resizeHandle:SimpleButton = new SimpleButton(); // resize handle
private var x:Number; // sticky x
private var y:Number; // sticky y
private var height:Number; // sticky height
private var width:Number; // sticky width
private var body:String; // sticky body
private var sprite:Sprite = new Sprite();
private static var RESIZE_HANDLE_SIZE:int = 20;

/* create sticky window */
public function Sticky():void {
var initOptions:NativeWindowInitOptions = new NativeWindowInitOptions();
initOptions.systemChrome = NativeWindowSystemChrome.NONE;
initOptions.transparent = true;
initOptions.type = NativeWindowType.LIGHTWEIGHT;

window = new NativeWindow(initOptions);
window.alwaysInFront = false;
window.stage.align = StageAlign.TOP_LEFT;
window.stage.scaleMode = StageScaleMode.NO_SCALE;

// layer for transparent editor
sprite.blendMode = BlendMode.LAYER;
window.stage.addChild(sprite);

// for edit area
editor = new TextField();
editor.x = editor.y = 0;
editor.selectable = true;
editor.border = false;
editor.type = TextFieldType.INPUT;
editor.multiline = true;
editor.background = true;
editor.wordWrap = true;
editor.backgroundColor = 0xE6E082;
editor.alpha = 1;
sprite.addChild(editor);

// window position of center
window.x = 0.5 * Capabilities.screenResolutionX - 300 * 0.5
window.y = 0.5 * Capabilities.screenResolutionY - 100 * 0.5

// size for window and edit area
window.width = editor.width = 300;
window.height = editor.height = 100;

// resize for window and edit area
window.stage.addEventListener(Event.RESIZE, function(e:Event):void {
resized();
});

// move and resize
window.stage.addEventListener(MouseEvent.MOUSE_DOWN, function(e:MouseEvent):void {
var x:Number = e.stageX;
var y:Number = e.stageY;
if (y > window.height - RESIZE_HANDLE_SIZE && x > window.width - RESIZE_HANDLE_SIZE) {
//window.startResize(NativeWindowResize.BOTTOM_RIGHT);
} else {
window.startMove();
}
});

window.stage.addEventListener(MouseEvent.MOUSE_UP, function(e:MouseEvent):void {
if (isChanged()) {
save();
}
});

editor.addEventListener(FocusEvent.FOCUS_OUT, function(e:FocusEvent):void {
if (isChanged()) {
save();
}
});

} // end of function Sticky

// show window
public function show():void {
// create close button
button.x = window.width - 15;
button.y = 5;
button.upState = createBox(0xE6E082, 10, 0.3);
button.overState = createBox(0xE6E082, 10, 1);
button.downState = createBox(0xCCCCCC, 10, 1);
button.hitTestState = button.upState;
button.addEventListener(MouseEvent.CLICK, function(e:MouseEvent):void {
var request:URLRequest = new URLRequest("http://stickynotes.heroku.com/stickies/" + id + ".xml");
request.method = 'DELETE';
var loader:URLLoader = new URLLoader();
loader.load(request);
window.close();
});
window.stage.addChild(button);

// create resize handle
resizeHandle.x = window.width - 15;
resizeHandle.y = window.height - 15;
resizeHandle.upState = createResizeHandle(0xE6E082, 10, 0.3);
resizeHandle.overState = createResizeHandle(0xE6E082, 10, 1);
resizeHandle.downState = createResizeHandle(0xCCCCCC, 10, 1);
resizeHandle.hitTestState = button.upState;
resizeHandle.addEventListener(MouseEvent.MOUSE_DOWN, function(e:MouseEvent):void {
window.startResize(NativeWindowResize.BOTTOM_RIGHT);
});
window.stage.addChild(resizeHandle);

window.visible = true;
}

// call after window resized
public function resized():void {
editor.width = window.width;
editor.height = window.height;
button.x = window.width - 15;
button.y = 5;
resizeHandle.x = window.width - 15;
resizeHandle.y = window.height - 15;
}

// save sticky
public function save():void {
// create URL of sticky
var request:URLRequest
if (!id) {
request = new URLRequest("http://stickynotes.heroku.com/stickies.xml");
request.method = 'POST';
} else {
request = new URLRequest("http://stickynotes.heroku.com/stickies/" + id + ".xml");
request.method = 'PUT';
}

// create paramater for edit
var variables:URLVariables = new URLVariables();
variables['sticky[x]'] = window.x / Capabilities.screenResolutionX;
variables['sticky[y]'] = window.y / Capabilities.screenResolutionY;
variables['sticky[width]'] = window.width;
variables['sticky[height]'] = window.height;
variables['sticky[body]'] = editor.text;
request.data = variables;

// send
var loader:URLLoader = new URLLoader();
loader.addEventListener(Event.COMPLETE, function(e:Event):void {
var xml:XML = new XML(e.target.data);
if (!id) {
id = xml.id;
}
updateStatus();
});
loader.load(request);
}

public function setAlwaysInFront(value:Boolean):void {
window.alwaysInFront = value;
if (value) {
editor.alpha = 0.85;
} else {
editor.alpha = 1;
}
}

public function updateStatus():void {
// sticky status
x = window.x;
y = window.y;
width = window.width;
height = window.height;
body = editor.text;
}

/* private methods ***********/

// close button
private function createBox(color:uint, radius:Number, vis:Number):Shape {
var xShape:Shape = new Shape();
xShape.graphics.lineStyle(1, 0x000000);
xShape.graphics.beginFill(color);
xShape.graphics.drawRect(0, 0, radius, radius);
xShape.graphics.moveTo(0, radius);
xShape.graphics.lineTo(radius, 0);
xShape.graphics.moveTo(radius, radius);
xShape.graphics.lineTo(0, 0);
xShape.graphics.endFill();
xShape.alpha = vis;
return xShape;
}

private function createResizeHandle(color:uint, radius:Number, vis:Number):Shape {
var xShape:Shape = new Shape();
xShape.graphics.lineStyle(1, 0x000000);
xShape.graphics.beginFill(color);
xShape.graphics.moveTo(0, radius);
xShape.graphics.lineTo(radius, 0);
xShape.graphics.lineTo(radius, radius);
xShape.graphics.lineTo(0, radius);
xShape.graphics.endFill();
xShape.alpha = vis;
return xShape;
}

// is paramater changed
private function isChanged():Boolean {
return (x != window.x || y != window.y ||
width != window.width || height != window.height ||
body != editor.text);
}

} // end of class Sticky

} // end of package

2008/03/11

CentOS 4.6 で、ZABBIX 1.4.4

社内のサーバ監視に、ZABBIX日本語サイト)を使用しているのですが、ZABBIX 1.4 では、「WEB monitoring」というWebサイトを簡単に監視できる機能が搭載されたようなので、この機能を使いたいがため、1.1.3 からアップデートしてみました。


New in ZABBIX 1.4 FEATURES日本語サイト


  • Installation Wizard

  • Installation Wizard automatically checks pre-requisites, database connectivity and generates configuration file for WEB front end.




  • Support of new database engines

  • Support of SQLite has been implemented. It allows use of ZABBIX in embedded environments.




  • WEB interface improvements

  • Speed and usability of WEB interface has been improved very much.




  • New notification methods

  • Native support of Jabber messaging has been introduced.




  • Distributed monitoring

  • ZABBIX distributed monitoring is made for complex environments consisting of different locations.
    ZABBIX supports monitoring of unlimited number of nodes. Centralized configuration allows easy configuration of all nodes from a single location.




  • Auto-discovery

  • ZABBIX distributed monitoring module allows easy deployment of ZABBIX systems. The discovery support IP ranges, service checks, agent and SNMP checks for efficient auto-discovery.




  • Many-to-many template linkage

  • More flexible host-template linkage saves time and make configuration of hosts more flexible and straight forward.




  • Database watchdog

  • ZABBIX server will automatically warn group of users if database is down and continues normal operations when database is back.




  • WEB monitoring

  • WEB monitoring module allows flexible and easy monitoringof availability and performanceof WEB sites and WEB based applications. It supports passing of GET and POST variables.




  • XML data import/export

  • New XML data import and export functionality is an excellent way of sharing templates, hosts configuration and items/triggers related information.




  • Support of Windows Vista

  • ZABBIX Windows agent supports Windows Vista, both 32 and 64 bit versions.




  • More flexible actions

  • Multiple operations (notifications, script execution) per action are supported. Choice of action calculation algorithm was introduced.




  • Server-side external checks

  • Server-side external checks can be used to introduce custom checks executed on ZABBIX server side.




  • New user permission schema

  • Old user permission schema is no longer support. It was replaced by new more efficient, yet simple, schema working on level of user groups and host groups.




  • Support of hysteresis

  • ZABBIX support use of different trigger expressions for going to ON and OFF states.




  • Support of slide show

  • Several screens can be grouped into a slide show for better presentation.




  • ZABBIX server can spread load across several servers

  • Groups of server side processes (discoverer, poller, HTTP poller, trapper, etc) can be located on different physical servers for better performance and availability.




  • Other improvements

  • See Release Notes for a complete list of improvements.




CentOS 4.6 の環境で、いくつか躓いたところがあったので、メモです。


CURLのバージョン


Web Monitoring機能を有効にするには、--with-libcurlオプションを付けてインストールします。


$ ./configure --enable-server --with-mysql --with-net-snmp --with-libcurl

すると、次のようなエラーが出て止まってしまいます。


checking for libcurl >= version 7.13.1... no
configure: error: Not found Curl library

CentOS 4.x では、curlのバージョンは7.12なのでダメなようです。

いろいろ調べてみると、結局、curlを自身でバージョンアップするしかないようです。

以下のようにして解決です。


centosinstall

$ cd ~/src/
$ wget ftp://ftp.planetmirror.com/pub/curl/libcurl4-devel-7.16.2-1.i386.rpm
$ wget ftp://ftp.planetmirror.com/pub/curl/libcurl4-7.16.2-1.i386.rpm
$ sudo yum install openssl096b
$ sudo yum remove curl-devel
$ sudo rpm -i libcurl4*


PHP4


ZABBIXのフロントエンドはPHPなのですが、やっとインストールが成功して管理画面にアクセスすると以下のエラーが発生。


PHP Parse error: parse error, unexpected T_STATIC, expecting T_OLD_FUNCTION or T_FUNCTION or T_VAR or '}' in /u02/zabbix/web/include/copt.lib.php on line 112

調べてみると、PHP5用のコードになっているのが原因のようです。

以下のようにして解決です。


Blank web install - ZABBIX Forums

includes/copt.inc.php を修正し、'static function' を 'function' にすべて置き換えます。


$ vi includes/copt.inc.php
:%s/static function/function/g


これで、無事にアップデートすることができました。

2008/03/09

RubyでGoogle Data APIs

Googleが提供するさまざまなサービスには、Google Data APIsを利用して、データの取得や更新を行うことができます。


現在、以下のサービスへのAPIが提供されています。



JavaScriptからSpreadsheetsを操作できないかと調べてみたのですが、JavaScript client libraryは、現在、Google Calendar と Bloggerのみのようです。


そこで、Rubyのライブラリを探してみると、gdata-rubyというライブラリがありました。
ただ、このライブラリは現在開発がストップしているらしく、SpreadsheetとBloggerのみに対応しているようです。


というわけで、せっかくなのでRubyでGoogle Spreadsheets data APIを試してみました。


ruby-gdataのインストール


gem install GData

書き込みテスト


#!/usr/bin/env ruby

require 'gdata/spreadsheet'

gdata_user = 'xxx@gmail.com'
gdata_pass = 'xxx'
gs_key = 'xxx'

gs = GData::Spreadsheet.new(gs_key)
gs.authenticate(gdata_user, gdata_pass)

gs.add_to_cell 'sin(0.2)'

残念ながら、使用感はいまいちでした。他によいライブラリはないのでしょうか・・・?

2008/02/29

Google Chart API を使ってみる

しばらく前に、Google Chart API が公開されたのですが、後れ馳せながら使ってみました。


Google Chart APIは、Google Static Maps APIと同じようにグラフを画像(PNGフォーマット)で返してくれるAPIです。(Google Chart APIが先ですが・・)


Google Chart API - Hello World
http://chart.apis.google.com/chart?cht=p3&chd=s:hW&chs=400x200&chl=Hello|World


Google Chart API のURLは、次の形式になっていて、parameters部分は以下のようになっています。※グラフの種類によって違ってきます。



http://chart.apis.google.com/chart?parameters


chs

グラフのサイズ

e.g. 400x250

※最大値は1000x1000

chd

グラフのデータ

e.g. s:helloWorld

データをエンコードした文字列を設定します

cht

グラフの種類

e.g. lc

chxt

軸ラベルの表示

e.g. x,y

chxl

軸ラベル

e.g. 0:|Mar|Apr|May|June|July|1:||50+Kb


上記の例だと、このようなグラフになります。



http://chart.apis.google.com/chart?chs=400x250&chd=s:helloWorld&cht=lc&chxt=x,y&chxl=0:|Mar|Apr|May|June|July|1:||50+Kb


表示できるグラフの種類は、円・棒・折れ線グラフはもちろん、面グラフや散布図といったものまで表示でき、大概のグラフは表示できそうです。


これまでグラフを貼り付けるのに、Excelで作成したものを貼り付けたり、Flashでやっていたものが、簡単に貼り付けられるようになったのは素晴らしいと思います。ただ、今のところ(2008/02/29現在)、ラベルに日本語はうまく表示できないようなのがちょっと残念。


なお、利用にあたっては、ユーザあたり50,000リクエスト/日(query limit of 50,000 queries per user per day)という制限があります。

2008/02/28

Google Static Maps APIを使ってみる

先日、より簡単にGoogleマップを表示できるようになる、Google Static Maps API公開されました。


特定の地図をimageデータ(GIFフォーマット)として返してくれるようで、いろいろと使い道があるのではないでしょうか。(特にモバイル)


というわけで、さっそく使ってみました。


River Stone Inc.


携帯で表示(240x270)
QRコード
http://www.r-stone.net/blogs/ishikawa/google_static_maps_api_mobile.html


使い方は非常に簡単で、Static Map Wizardで手軽に作成できます。


Google Static Maps API のURLは、次の形式になっていて、parameters部分は以下のようになっています。



http://maps.google.com/staticmap?parameters


center(必須)

マップの中央の座標

e.g. 40.714728,-73.998672

zoom(必須)

ズームレベル

e.g. 17

size(必須)

画像のサイズ

e.g. 500x400

※最大値は512x512

maptype(オプション)

roadmap(デフォルト):通常のマップ

mobile:表示文字等が簡略化されたモバイル用

markers(オプション)

マーカーの位置,色,文字

※"|"で区切ることで複数マーカーが可能

{latitude},{longitude},{color}{alpha-character}

e.g. 40.702147,-74.015794,blues|40.711614,-74.012318,greeng

key(必須)

APIキー

Google Maps APIと同じ


なお、利用にあたって、1ユーザ(1IPアドレス)当たり、表示は1日1,000種類の画像までという制限(※原文によると「1000 unique (different) image requests per viewer per day」)があるので要注意です。