2009年6月28日日曜日

こんな週もあるか

いや~、疲れた。朝からずっとスケート履きっぱなしはいつ以来だろ。レフェリー自体がずいぶん久しぶりだった気がするな~。なによりノブヒコにあうのは2年ぶり?でも、久しぶりに一緒にホッケー出来て楽しかった。そんな久しぶり続きで箸も持てないほど疲れたけど、1試合も勝てなかったっていうのが一番の驚きかも。全然負けてる気がしないのに、結果2敗2分け?すんごい面白かったから、いいんだけどさ。なんか、こう、あれれ~、な感じだよね。

Google Page Creatorの移行をさっさとすませろと督促メールも来たことだし、サーバー引越中...。なんかいろいろやることあるな~。

2009年6月21日日曜日

RouteHandlerはシングルインスタンス

いや~、サブジェクトの件、全然知りませんでした。

ASP.NET MVCはControllerクラスのAction実行でViewを返すじゃないっすか。ControllerはDefaultControllerFactoryがインスタンスを作るんですよね。で、IControllerさえ実装してればそれはつまりControllerであると。なので、IController.Executeだけを実装したサンプルを書いて、まずはそれを確認。

using System;
using System.Web.Mvc;

namespace RouteHandler.Controllers
{
  public class SimpleController : IController
  {
    private DateTime _createTime = DateTime.Now;
    public void Execute(System.Web.Routing.RequestContext requestContext)
    {
      requestContext.HttpContext.Response.Write(string.Format(@"<html>
<body>
<h1>Simple Controller</h1>
アクションやら完全無視!<br />
{0}
</body>
</html>", _createTime));
    }
  }
}

簡単なコードです。特にこれと言って変なところは無いですね。ただ、こういう使い方をするとActionなんて無いし、この中に定義したとしても、RouteDataにアクションへの情報を参照してInvokeするコードが無いので無視するだけです。ようはActionInvokerに相当する機能がないって事です。

わざわざ_createTimeを取っているのは、この後の確認作業で理由が判明する予定。SimpleControllerという名前なので、デフォルトのルーティング設定だけで、このコントローラは実行まで行くことが出来ます。動かしたのが↓この画面。

route1

画面を拡大してみると分かると思うけど、URLは単純に/simpleです。で、ですよ、Controllerは単純にIHttpHandlerなんだから今度はIHttphandlerを実装してしまえば、DefaultControllerFactoryを経由させないでも出来るってことデスよ。でも、その為にはRouteHandlerが必要になります。いきなりの展開で話が分かりにくいっすね。

ルーティングモジュールがFrontControllerとしてすべての要求を全部受け入れてくれるじゃないですか(そういう言い方しないですかね)。で、その中で登録されてるルーティング情報に従ってRouteDataに色々なデータを突っ込んでくれる。RouteCollectionExtensions.MapRouteが色々セットアップしてUrlRoutingHandler(UrlRoutingHandler クラス (System.Web.Routing))を使ってそこから先はMVCの方で好きなようにやりたまえと責任が委譲されます。MvcRouteHandlerとMvcHandlerがその後を引き継いで、ごにょごにょと進んでいくけど、UrlRoutingHandlerの所を自分で実装したものに差し替えちゃえば、ControllerFactoryなど経由せずIHttpHandlerを直に使えるっていう流れなんですが、こうなるとMVCじゃなくてただのRouting利用っすね...。まぁ、いいや。UrlRoutingHandlerはIRouteHandlerの実装なので、それを実装。

using System;
using System.Web;
using System.Web.Routing;

namespace RouteHandler.Libraries
{
  public class SimpleRouteHandler : IRouteHandler
  {
    private DateTime _createTime = DateTime.Now;

    public IHttpHandler GetHttpHandler(RequestContext requestContext)
    {
      var routeData = requestContext.RouteData;
      routeData.DataTokens["routeTime"] = _createTime;

      var httpHandler = new SimpleHttpHandler(requestContext);
      return httpHandler;
    }
  }
}

ただGetHttpHandlerを実装するだけデス。返すハンドラはSimpleHttpHandler。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Routing;

namespace RouteHandler.Libraries
{
  public class SimpleHttpHandler : IHttpHandler
  {
    private DateTime _createTime = DateTime.Now;
    private DateTime _routeTime;
    
    public SimpleHttpHandler(RequestContext requestContext)
    {
      var routeData = requestContext.RouteData;
      _routeTime = (DateTime)routeData.DataTokens["routeTime"];
    }

    public void ProcessRequest(HttpContext context)
    {
      context.Response.Write(string.Format(@"<html>
<body>
<h1>Simple RouteHandler</h1>
HttpHandlerを直接使う<br />
RouteHandler : {0}<br />
HttpHandler : {1}<br />
</body>
</html>", _routeTime, _createTime));
    }

    public bool IsReusable
    {
      get { return false; }
    }
  }
}

ProcessRequestを実装しただけですが、せっかくRouteHandlerは自前なのでRouteDataを参照したいじゃないですか。なので、コンストラクタでRequestContextを受け取るようにしておいて、RouteDataを取り出しておきます。このRouteHandlerをRouteTableに登録。

      routes.Add(new Route("route", new SimpleRouteHandler()));

Global.asaxのRegisterRoutesに追加するだけです。

これを実行したのが↓これ。

route2 

時間2箇所だしてるので分かるかな~、とは思うんですが、RouteHandlerが生成された時間と、HttpHandlerが生成された時間が少しずれてます。もう少し時間をおいて試したのが↓こっち。

route3

HttpHandlerは毎回違う時間(IsReusable=falseかどうかは見ずに毎回newしてる)になってるけど、RouteHandlerは1個前の写真の時間と同じです。ピンと来た?ちなみに最初のSimpleControllerは毎回時間が更新されます。

そうなんですよ、RouteHandlerのインスタンスは使い回されるんですよ。しかも沢山のスレッドから同時に。なので、RouteHandlerの中でスレッドセーフじゃないクラスを使ったりするとちょっと切ない思いをすることになります。データベースにアクセスしようとかしたらダメですよ...。ロックなんてしたら、これまたスケールしなくなるし。

RouteHandlerにはシンプルなロジックだけを書くようにしましょう、という話でした。

2009年6月20日土曜日

暑すぎだね~

何となく、最近週末にしかエントリ出来てない気がするな~。新人だからそれもやむなし。ボスには好きなだけ(情報漏らさない限り)書いていいと言われてるけど、なかなか眠しなウィークデイっす。今日は珍しく午後から試合ってことで、朝はのんびり過ごしてたんだよね。そろそろ行くかと外に出たら、アレだね、夏だね。オレ in サマーだね。

前回の試合でウィールがひどくて滑れないと、さんざん言い訳をかましてたけど、今回はウィールも新調し、ブレードもおニューで、もう道具のせいには出来ないっていう状況を用意しました。なんせ今回の相手はヤマちゃんもいるチームだしね!うひゃひゃ。この時期恒例の首都高での気温チェックをしてみたら28度になってて、途中行くのやめてマジで帰ろうかと心が折れかかった。

そういえば、iPhone OS 3.0出てるじゃないですか。速攻でアップグレードしたんですよ。いろいろ新機能があったりしてるみたいだけど、やっぱり機械を新しいのにしないとベストな状態で使えてる気がしない。なんとなくレスポンス悪かったりしてる感じが否めない。日本語入力の反応(変換候補の選択が特に反応悪し)悪くないですか?それでも、コピペが出来るのは嬉しいし、曲順が日本語順だったり、細かい表示が改善されてたりして、凄くいいのは確かっす。テザリングが標準で使えないのが残念だけど、最近はめっきりノートPC持ち歩かないから、そこはまぁ、いっか、みたいな。出来るに越したことはないけど、調べたりしてまで使いたいかっていうとそれほどでもないか。

最近、電車通勤してて気がついたんだけど、今履いてるNIKE FREEは少しでも雨が降るとすぐにしみてきて、靴下がびちょびちょになる。もう半年くらい履いてるのに、全然知らなかった...。長靴買おうかな。

そうだ、ホッケーの試合の話だった。なんだかんだヤマちゃんもショウ君もいなくて、ありゃりゃだったけど、ゴーリーが鬼のようなセービングで全然点が取れず、まさかの敗北かと思ったけど、なんとかオーバータイムで勝利。危なかった。53本うって5点っすよ...。どんだけ止めれば気が済むんですか。で、この試合の内容はミサキ選手が後日感想文を送ってくることになってるので、詳細はこうご期待!

試合後、サヨちゃんがいたから「スズキ君は~?」って尋ねたら「10日も仕事休んだから今日は仕事に行ってる~」だって。スゴイよね、世界選手権だもんね。日本代表だもんね。「じゃ~、無事帰ってきたんだね。ケガもなく。」って言ったら「いや、それが...」と、どうも大変な事になってたみたいで、軽く記憶が飛んだりするような危ない感じの脳しんとうを起こしたらしい(とても危険な反則をされたみたい)。昨日は元気に戸塚でまたホッケーしてたらしいけど。今度ちょっと話を聞かせてくれたまえ。

2009年6月13日土曜日

全然意識したこと無かったけど

ASP.NET MVCだからどうのこうのっていう話ではないんだけど、ちょっと衝撃。

ASP.NET MVC Routing vs. Reserved Filenames in Windows - Stack Overflow

↑ここで初めて目にした時は"へぇ~"くらいだった。

bitquabit - Zombie Operating Systems and ASP.NET MVC

↑ここで2回目に目にした時は"マジやべ~"に変わった。デフォルトルート設定のままCom1Controllerを作ってアクセスしてみた。

ng1

ぬふ。404ですね。普通に開発してたら、Com1Controllerなんて作ったりしないから平気だろう、なんて甘いこと考えてたらダメですよ!

続いて、HomeControllerのIndexアクションを以下のようにしてみましょう。普通です。

    public ActionResult Index(string id)
    {
      ViewData["id"] = id;

      return View();
    }

で、Home/Index.aspxにこれまた以下のようなコードを書いたとしましょう。

  <p>
  id="<%= Html.Encode(ViewData["id"]) %>"
  </p>
  
  <div></div>
  <dl>
    <dt>ダメなパターン</dt>
    <dd>
    <%
      foreach (var id in new[]{"COM","LPT"}.SelectMany(
              l=>Enumerable.Range(1, 9).Select(r=>l+r))
                           .Union(new[]{"CON","AUX","PRN","NUL"}))
        Response.Write(Html.ActionLink(id, "Index", new { id }) + " ");
    %>
    </dd>
    <dt>いいパターン</dt>
    <dd>
    <%
      foreach (var id in new[] { "COM0", "COM1029", "id", "PRINTER", "NULL" })
        Response.Write(Html.ActionLink(id, "Index", new { id }) + " ");
    %>
    </dd>
  </dl>

ワクワクしますな。ドキドキが止まりませんな。

ng2

こんなのが表示されますね。もうあからさまに怪しいのが見てとれます。早速リンクをクリックしてみようじゃないですか。

まずは、下段の"COM0"。普通上段からだよ...。まぁ、いいでしょう。

ng3

ちゃんと出ます。続いて"COM1209"、"NULL"も試してみたけどちゃんと出ます。

今度は上段の"COM1"。

ng4

ぎゃふーん!!

でも、変な値が出るわけじゃないから、まぁいいか。そんなURL入れるのが悪い!なんて思ってたら変な障害出たりしてあたふたすることになりかねないですよ!だって、IDに文字列入力許すような設計って普通じゃないですか。んで、ルーティングのパラメータにID含むようなURL設計って当たり前すぎるじゃないですか。でも、ID入力時に"COM1"やら"NUL"やら"AUX"やらはじくような処理を入れるのが当たり前なわけじゃないじゃないですか。当たり前なんですか?そーだったらすいません。

何となくRouteCollection.RouteExistingFiles プロパティ (System.Web.Routing)でtrueにしてみたけど、ダメだったから、そういうもんだとして入力値のチェックをちゃんとやろうと思うところです。

2009年6月7日日曜日

Json.NETとStringTemplateでお気楽HTML出力

暇だったんですよね。で、暇つぶしにJSONからStringTemplateを通してHTMLをはき出させてみたんです。何となくですけど、適当にデータを定義しておいて、テンプレートに当てはめてHTMLを出力するっていうツールがないものかと探してみたんだけど、どーにも楽ちんそうなのが見あたらなくて。あ、ちなみに5月の話です。

データはXMLでも良かったんだけど、書くのが面倒になるのもやだし、テンプレート解釈とかは作りたくないし、ってことで、Json.NET - James Newton-KingStringTemplate Template Engineで書いてみたっす。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using Antlr.StringTemplate;
using System.IO;
using Newtonsoft.Json.Linq;

namespace STHtml
{
  public class STGenerator
  {
    private const string INPUT_PATH = "input";
    private const string ROOT_PROPERTY = "templateItems";
    private const string FILENAME_PROPERTY = "output_path";
    private const string FILENAME_FORMAT = "output{0}.txt";

    private STSetting _setting;

    public STGenerator(STSetting setting)
    {
      _setting = setting;
    }

    public void Execute(IOutput output)
    {
      var group = new StringTemplateGroup("htmlTemplates", INPUT_PATH);
      var json = LoadJSON();

      if (json == null)
        return;

      // 
      var list = (from item in json[ROOT_PROPERTY]
                  select item).ToList();

      foreach (var item in list)
      {
        // テンプレートの読み込み
        var st = group.GetInstanceOf(_setting.TemplateFileName);

        // ファイル名は?
        var filePath = GetOutputFilePath(item, list.IndexOf(item));

        // 生成
        var generated = GenerateFromJSON(st, item);
        if (generated == null)
          return;

        // 出力
        output.Print(filePath, generated);
        Console.WriteLine("output -> " + filePath);

      }
      Console.WriteLine("{0} file(s) template generated.", list.Count());
    }

    private string GetOutputFilePath(JToken item, int itemIndex)
    {
      // ファイル名は?
      var fileName = (from t in item
                      select item[FILENAME_PROPERTY]).FirstOrDefault();
      var filePath = Path.Combine(_setting.OutputPath,
        fileName != null ?
          fileName.ToString().Replace("\"", "") :
          string.Format(FILENAME_FORMAT, itemIndex)
      );

      return filePath;
    }

    private JToken LoadJSON()
    {
      string jsonText;
      JObject json = null;

      try
      {

        jsonText = File.ReadAllText(Path.Combine(INPUT_PATH, _setting.JsonFileName));
        json = JObject.Parse(jsonText);
      }
      catch (Exception e)
      {
        Console.WriteLine("データファイルの書式が間違ってるか、ファイルがないか...");
        Console.WriteLine(e.Message);
        json = null;
      }

      return json;
    }

    private string GenerateFromJSON(StringTemplate st, JToken json)
    {
      try
      {
        // JsonからDictionaryに変換
        // JObjectからそのままはStringTemplateが無理さ
        var dict = JsonToDictionary(json) as Dictionary<string, object>;
        foreach (var kv in dict)
        {
          st.SetAttribute(kv.Key, kv.Value);
        }
      }
      catch (Exception e)
      {
        Console.WriteLine("変換に失敗したよ。テンプレートがおかしいと思われる。");
        Console.WriteLine(e.Message);
      }

      return st.ToString();
    }

    private object JsonToDictionary(JToken token)
    {
      Dictionary<string, object> result = new Dictionary<string, object>();

      // なんか美しくないね。
      // 値セットと値戻しが同列だしな~。まぁ、いっか。
      foreach (var node in token)
      {
        if (node is JProperty)
        {
          // プロパティ型(name:value)なら取得
          var prop = node as JProperty;
          // 配列([...])かオブジェクト({...})なら再帰
          if (prop.Value.Type == JsonTokenType.Array ||
              prop.Value.Type == JsonTokenType.Object)
          {
            result[prop.Name] = JsonToDictionary(prop);
          }
          else
          {
            // その他の値型なら文字列化
            var value = prop.Value.ToString()
                                  .Replace("\"", "");

            result[prop.Name] = value;
          }
        }
        else if (node is JArray)
        {
          // 配列型なら戻す(再帰の時にしか処理しないもん)
          var arr = node as JArray;
          var list = new List<object>();
          foreach (var item in arr)
          {
            list.Add(JsonToDictionary(item as JToken));
          }
          return list;
        }
      }

      return result;
    }
  }
}

説明するのが面倒なんでソースです。こんな適当な感じでもそれなりに動くよ~。JsonからHTMLへの埋め込み時にEncodeかかってないからその辺は気をつけましょう。

使い方なんですけど(使う人がいるとは思えないけど)。

まずはinputフォルダのdata.jsonファイルにデータを書き込んでいきます。例えば↓こんな感じです。

{templateItems:
  [
    {
      output_path:'test1.html',
      title:'ページ1',
      subject:'うぎょぎょ<br />ぼへ',
      gallery:[
        {src:'1.jpg', alt:'', title:'説明文を書きましょう'},
        {src:'2.jpg', alt:'', title:'説明文を書きましょう'},
        {src:'3.jpg', alt:'', title:'説明文を書きましょう'}
      ]
    },
    {
      output_path:'test2.html',
      title:'ページ2',
      subject:'うぎょぎょ<br />ぼへ',
      gallery:[
        {src:'1.jpg', alt:'', title:'説明文を書きましょう'}
      ]
    },
    {
      output_path:'test3.html',
      title:'ページ3',
      subject:'うぎょぎょ<br />ぼへ',
      gallery:[
        {src:'1.jpg', alt:'', title:'説明文を書きましょう'},
        {src:'2.jpg', alt:'', title:'説明文を書きましょう'}
      ]
    }

  ]
}

全体が一つのObjectです。で、ルートではtemplateItemsっていう名前の配列を定義がルールです。templateItems配列に入れる各オブジェクト(アイテムオブジェクト)の書式は気をつけて統一する必要あり。こんな時にJSONスキーマが役立つんだろうな~。面倒なので気をつけるっていうルールで。アイテムオブジェクトはどんな形式でもOK、のはず。最初に提示したソースのJsonToDictionary関数が再帰でその辺上手くやってくれるようにしてます。でも、JArray型とJProperty型以外は想定してないので、まぁ、その辺は雰囲気で。

あ、アイテムオブジェクトに絶対に入れなきゃ行けないのが出力ファイル名をセットしたoutput_path。上記の例だとoutputフォルダにそのまま出す感じなんですが、例えば"output_path:’folder1/default.html’"とかってしておくと、outputフォルダ内にfolder1フォルダを作成して、その中にdefault.htmlを出力します。

次にテンプレートファイルとして↓こんなのを用意。

<!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">
<head>
  <meta http-equiv="content-type" content="text/html; charset=utf-8" />
  <meta name="keywords" content="">
  <title>$title$</title>
</head>
<body>
<h1>$title$</h1>
<div>
  <h2>$subject$</h2>
  <ul>
    $gallery:{g|
    <li><img src="$g.src$" alt="$g.alt$" title="$g.title$" /></li>
    }$
  </ul>
</div>
</body>
</html>

後は実行するだけ。そうすると↓こんなのが出力されました。

sthtml1 sthtml2

またしてもプロジェクトファイルは添付しておくのでご自由に。

ズルズル滑る

いや~、負けちゃったね~。ボロッカスっすね~。そんな時もあるよね~。今期3回目のオグさんゴーリーもお疲れちゃんでしたね。内容的にはそんな悪くないと思うよ。

あれじゃない?今日は来れなかったけどコレでイマイちゃんも来たら女子6人だもんね。スゴイね。男子陣がんばんないとな~。個人的に前回もそうだったけどウィールがひどい。今日は雨で湿度が高かったっていうのもあるかもしれないけど、あまりにもひどい。たぶんね、傍目には単に「たけはらも衰えたな」と見えるかもしれないけど、もうね、ひどいよウィール。雨の戸塚みたいな感じよ。誇張抜きで。止まれない曲がれないダッシュ効かない。ひどいモンでした。前回よりもズルズルでよ~。参ったね。上半身は反応しても下半身が滑ってついてこないもどかしさ。あまりにもひどいから試合後新しいウィール買いました。次は普通に滑れるっす。セト君ゴーリーには期待出来ないから、とりあえずヤマちゃんには何もさせないようにちゃんと押さえ込むね。うしし。楽しみ~。

全然話は変わるんだけどね、最近「リファクタリング・ウェットウェア」いう本を読みまして。これがさ~、驚きの面白さ。最初の章で"ドレイファスモデル"っていう話が書いてあって、よく上手く言葉でここまで表現できるな~、とちょっと感動。自分で自分がドレイファスモデルで今どの段階だと思ってるかはちょっとナイショ。てへ。その後も脳がどういうふうに反応するとか、コンテキストが大事とか、二次無能力なんていう定義がちゃんとあったりするんだ~、みたいなとかね。

あとね~、世代間の"4種の原型"も。各世代での世界の見方が繰り返すっていうのとか。アンディーハントも最初のほうに書いてるけど、開発者が読む本じゃなくて誰が読んでも面白く読めると思う。ドレイファスモデルで特定のレベルを超えてる人なら特に。

絶対読んで損しないから読んでみたらいいと思うけど、6章の最初に引用してるマークトウェインの引用「ある経験から得た知識をすべてだと思い込み、先に進むのを躊躇すべきではない。熱いストーブの蓋に座った猫は、同じように熱いストーブの蓋には二度と座らない。それはよいのだが、冷たい蓋にもけっして座ろうとしなくなるだろう。」が上手にテーマを表現できてる気がするな~。ちなみに同じく6章で紹介されてるインナーゲームも読んだことがあって、その時ハンマーで殴られたような衝撃を受けたのを思い出した。

2009年6月1日月曜日

今日から6月

で、今日から新しい会社勤務。

いきなり電車乗り間違えて、遅刻しそうになって駅からダッシュ。この緊張感がたまらない。楽しくコード書き続けます。

dotnetConf2015 Japan

https://github.com/takepara/MvcVpl ↑こちらにいろいろ置いときました。 参加してくださった方々の温かい対応に感謝感謝です。