何度やっても同じ

ただの日記

BlobstoreService + slim3 でプチはまり

Blobstoreにファイルをアップロードするのといっしょにサーバにパラメータを渡す必要があったので、実現方法を二つほど考えたのですが、

  • multipartで送る
  • Blobアップロード後に呼び出されるパスにあらかじめクエリ文字列でパラメータを埋め込んでおく

どちらの方法でも渡せないのでした。いや、サーバには渡せているのだけど、どちらもslim3によって処理されないのです(前者の方法は開発環境では何故か問題ないみたい)。なので、アップロード後に呼ばれるコントローラは次のような状態です。別にこれでもいいんですけどね。。。

String id = asString("id"); // ぬるっと
String id = request.getParameter("id"); // これなら取得できる

これでは不満な場合、以下のように解決できそう。

まずこんなクラスを作ってですね。。。

public class BlobUploadedRequestHander extends RequestHandler {

  public BlobUploadedRequestHander(HttpServletRequest request) {
    super(request);
  }

  @Override
  public void handle() {
    super.handle();
    BlobstoreService blobstore = BlobstoreServiceFactory.getBlobstoreService();
    Map<String, List<BlobKey>> map = blobstore.getUploads(request);
    for (Map.Entry<String, List<BlobKey>> e : map.entrySet()) {
      request.setAttribute(e.getKey(), e.getValue());
    }
  }
}

アップロード後の処理をするコントローラを次のように。ついでに共通の親コントローラにasBlobKeyメソッドとか定義しておくと更にすっきり。

public class UploadController extends OreOreController {

  @Override
  protected RequestHandler createRequestHandler(HttpServletRequest request) {
    return new BlobUploadedRequestHander(request);
  }

  @Override
  public Navigation run() throws Exception {

    Validators v = new Validators(request);
    v.add("id", v.required(), v.longType());
    v.add("file", v.required());
    if (!v.validate()) {
      return forward("xxx.jsp");
    }

    long id = asLong("id");
    BlobKey blobKey = asBlobKey("file");

    // なんか処理
  }
}

GAEJ PDFのテキスト抽出

Apache PDFBoxを使ってPDFのテキスト抽出を行おうとすると、java.awtパッケージのクラス(GAEJのホワイトリストに含まれない)を使っている関係でエラーが発生します。テキスト抽出にjava.awt関連クラスは不要なので、使用個所を削りまくってGAEJ上で動作するjarを作りました。ベースにしたバージョンは1.7.1です。

デモサイトのソースコードslim3
public class UploadController extends Controller {

  @Override
  public Navigation run() throws Exception {

    BlobstoreService blobstore = BlobstoreServiceFactory.getBlobstoreService();
    Map<String, List<BlobKey>> map = blobstore.getUploads(request);
    List<BlobKey> keys = map.get("pdfFile");
    if (keys == null || keys.isEmpty()) {
      return html("<p>ファイルが選択されてないかも</p>", "utf-8");
    }

    BlobKey key = new BlobKey(keys.get(0).getKeyString());
    PDFParser parser = new PDFParser(new BlobstoreInputStream(key));
    parser.parse();
    PDDocument doc = parser.getPDDocument()
    PDFTextStripper stripper = new PDFTextStripper();
    response.setContentType("text/plain; charset=utf8");
    stripper.writeText(doc, response.getWriter());

    blobstore.delete(key);

    return null;
  }
}

GAEJ Blobstoreのでっかいファイルをダウンロード

BlobstoreService blobstore = BlobstoreServiceFactory.getBlobstoreService();
BlobKey key = new BlobKey(asString("key"));
response.setHeader("Content-disposition", "attachment; filename=hogehoge");
blobstore.serve(key, response);

何か理由があって自分でがんばるときは。

BlobKey key = new BlobKey(asString("key"));
BlobInfo info = new BlobInfoFactory().loadBlobInfo(key);
int size = (int) info.getSize();

response.setContentType(info.getContentType());
response.setContentLength(size);
response.setHeader("Content-disposition", "attachment; filename=" + info.getFilename());

BlobstoreService blobstore = BlobstoreServiceFactory.getBlobstoreService();
int start = 0;

do {
  int end = Math.min(size, start + BlobstoreService.MAX_BLOB_FETCH_SIZE) - 1;
  byte[] buf = blobstore.fetchData(key, start, end);
  response.getOutputStream().write(buf);
  start = end + 1;
} while (start < size);
        
response.flushBuffer();

counter-resetはスコープを構成する

連番を表示したくて下のようなCSSを書いたのですが、各行の先頭すべてに "1." と表示されてしまい、「???」となったので調べました。

<style>
#list li:before {
  counter-increment: item;
  content: counter(item) ".";
}
</style>
<ul id="list">
  <li>ひー</li>
  <li>ふー</li>
  <li>ほえー</li>
</ul>

とりあえずさっきのリストの各行に正しい番号を(上から順に "1.", "2.", "3." と)表示したければ、次のようなスタイル定義を追加すればいいわけですが、couner-resetプロパティが単にカウンターを0にリセットするだけではなさそうというのがこのことから見えてきます。

#list {
  counter-reset: item;
}

というわけで仕様確認しました

http://www.w3.org/TR/CSS21/generate.html#scope

カウンターのスコープ

あるカウンターのスコープは、ドキュメント内で最初にそのカウンターをcounter-resetした要素から始まり、その子孫要素と兄弟要素そして兄弟の子孫まで含まれる。

子孫要素がスコープとなるのはさっきのリストの例でよくわかります。兄弟もスコープに含まれるので、たとえば次のようなこともできますが、こんなことしないでリスト使えって感じです。だいたいの場合はcounter-resetするのに適切な親要素がありそう。兄弟要素がスコープに含まれることを説明できるもっと適切な例ないかな。

<style>
section h2 {
  counter-reset: item;
}
section p:before {
  counter-increment: item;
  content: counter(item) ".";
}
</style>
<section>
  <h2>メリット</h2>
  <p>お手軽</p>
  <p>安価</p>
  <h2>デメリット</h2>
  <p>太る</p>
  <p>イカ臭くなる</p>
</section>
スコープがネストする場合

ただし、そのスコープ内に同じ名前でカウンターを作成(つまりcounter-reset)している要素がある場合、その要素のスコープは親スコープから除外する。

たとえば次のようにリスト構造がネストしている場合、counter-reset:itemは2回実行されることになりますが、外側のulでリセットされたカウンター「item」と、内側のulでリセットされた同名のカウンターは別物で、それぞれ独自のスコープを構成します。

<style>
ul {
  counter-reset: item;
}
ul li:before {
  counter-increment: item;
  content: counter(item) ".";
}
</style>
<ul>
  <li>aaa</li>
  <li>bbb</li>
  <li>
    <ul>
      <li>child1</li>
      <li>child2</li>
    </ul>
  </li>
  <li>ccc</li>
</ul>

f:id:xfan:20121209155500j:plain

スコープ外でいきなりカウンターを参照した場合

どのcounter-resetのスコープにも含まれない要素でcounter-incrementなどを使ってカウンターを参照しようとした場合、その直前でcounter-resetがカウンタースコープを定義し、値を0にリセットしたように(ブラウザは)ふるまう(べき)。

最初の例で、リストの全行のカウントが1になってしまったのは、このルールによるものですね。

古いTwitterウィジェットのコードをメモっとく

新しいウィジェットでは検索キーワードを動的に変えたりできないですよね……。というわけで。featuresプロパティはカスタマイズされているのか何なのかよくわかりません。

プロフィール

<script src="http://widgets.twimg.com/j/2/widget.js"></script>
<script>
new TWTR.Widget({
  version: 2,
  type: 'profile',
  rpp: 3,
  interval: 6000,
  width: 'auto',
  height: 300,
  theme: {
    shell: {
      background: '#8ec1da',
      color: '#ffffff'
    },
    tweets: {
      background: '#ffffff',
      color: '#444444',
      links: '#1985b5'
    }
  },
  features: {
    scrollbar: false,
    loop: false,
    live: false,
    behavior: 'all'
  }
}).render().setUser('xfan').start();
</script>

キーワード検索

<script src="http://widgets.twimg.com/j/2/widget.js"></script>
<script>
new TWTR.Widget({
  version: 2,
  type: 'search',
  search: 'つぶらなカボス',
  interval: 6000,
  // title: '',
  subject: '「つぶらなカボス」に関するつぶやき',
  width: 'auto',
  height: 300,
  theme: {
    shell: {
      background: '#8ec1da',
      color: '#ffffff'
    },
    tweets: {
      background: '#ffffff',
      color: '#444444',
      links: '#1985b5'
    }
  },
  features: {
    scrollbar: false,
    loop: true,
    live: true,
    hashtags: true,
    timestamp: true,
    avatars: true,
    toptweets: true,
    behavior: 'default'
  }
}).render().start();
</script>

Twitter Bootstrapのfluidレイアウトで右側にあるカラムを、横幅が狭いときに上側に表示したい

こんなHTMLがあるとして……

<div class="row-fluid">
  <div id="content" class="span8">めいんこんてんつ</div>
  <div id="sidebar" class="span4">サイドバー</div>
</div>

ブラウザ幅767px以下で表示すると、サイドバー要素がメインコンテンツの下に表示されてしまうわけだけど、これをメインコンテンツの上に表示するにはどうすればよいかということで。

まずはHTML要素の現れる順番をひっくり返しておきます。

<div class="row-fluid">
  <div id="sidebar" class="span4">サイドバー</div>
  <div id="content" class="span8">めいんこんてんつ</div>
</div>

このままだとサイドバーが左に表示されてしまうので、サイドバーを強引に右へ移動させます。

#sidebar {
  float: right;
}

しかし、もともと右側にあるはずだった#contentはmargin-leftをもっているので、左端に余計なスペースができてしまい、逆にコンテンツとサイドバーの間の隙間がなくなってしまう。そこでこう。

#content {
    margin-left: 0;
}

おわり。

ちなみにコンテンツとサイドバーの要素はIDセレクタCSS定義しないとBootstrapのCSSに優先度負けてしまって!importantすることになってキモいので、ID設定だいじ!

ついでに、ブラウザ幅縮小時にサイドバーとコンテンツの間にマージンを入れたい場合は、こんなかんじ。

@media ( max-width : 767px) {
  #main {
    margin-top: 20px;
  }
  #sidebar {
    float: none;
  }
}

google日本語入力用、ピンインを声調付きで入力できる辞書

つくった。

http://dl.dropbox.com/u/523141/pinyin.txt

これ↑をインポート。
入力モードを「半角英数」にして(言語バーの表示が _A のようになる)使ってください。