vaguely

和歌山に戻りました。ふらふらと色々なものに手を出す毎日。

【Java】CompletableFuture で遊ぶ 1

はじめに

以前 JavaC# を使ってローカルファイルを開く、というのをやりました。

C# では(意味があったかどうかはともかくとして) async/await で非同期に処理を行っていましたが、これに近いことを Java で実現するにはどうすれば良いかな~というのが今回のお話です。

CompletableFuture を使う

ググってみたところ、 Java 8 から導入された CompletableFuture を使うのが良さそうだったので、試してみることにしました。

【before】App.java

package SearchContainedFiles;

import java.nio.file.Path;
import java.nio.file.Paths;
import SearchContainedFiles.FileLoaders.FileLoader;

public class App {
    public static void main(String[] args) {
        if(args == null || args.length < 2){
            System.out.println("Need two args");
            return;
        }
        for(Path file: FileLoader.Search(Paths.get(args[0]), args[1])){
            System.out.println(file.toString());
        }
    }
}

【after】App.java

package SearchContainedFiles;

import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.function.Supplier;
import SearchContainedFiles.FileLoaders.FileLoader;

public class App {
    public static void main(String[] args) {

        if (args == null || args.length < 2) {
            System.out.println("Need two args");
            return;
        }
        Supplier< Path[]> loadedPaths = () -> FileLoader.Search(Paths.get(args[0]), args[1]);
        CompletableFuture< Path[]> supply = CompletableFuture.supplyAsync(loadedPaths);
        
        try {    
            Path[] paths = supply.get();

            for(Path file: paths){
                System.out.println(file.toString());
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

これでローカルファイルを読み込む処理( FileLoader.Search() )が非同期(別スレッド)で実行されます。

呼び出すメソッドでは( async/await をつけるような)変更が必要なく、呼び出し元の方も単語としては await のような「待つ」といった意味合いのメソッドは見当たりません。

が、ちゃんと非同期の処理が完了するまで待った上で全部の処理が完了します。

どのようにしてこれらの処理が動いているのかを追ってみることにしました。

何をしているのか

まず、 CompletableFuture を含め、 Future を使った非同期処理というのは、 Future パターン と呼ばれるデザインパターンで実行されるとのこと。

Java本格入門 で挙げられている Future パターンの例では、下記 3 つのクラスが登場します。

  1. ExecutorService
  2. Future< String>
  3. Callable< String>

流れとしては下記のような感じだと解釈しています。
(実際の処理は本を見てもらうとして)

f:id:mslGt:20190522024939p:plain

では先ほどのサンプルコードが、これと同じような動きになっているのかを見てみたいと思います。

(Completable)Future が出てくるあたり完全に同じでないにしろ、近そうな予感はありますね。

ソースコード

ソースコードOpenJDKのサイト から入手できます。

今回は下記を ZIP 形式でダウンロードしてきました(ページ左にリンクがあります)。

コードを辿る冒険

今回の処理の中心となるのは下記のクラスです。

  • CompletableFuture.java
  • ForkJoinPool.java

これらはいずれも src > java.base > share > classes > java > util > concurrent にあります。

supplyAsync

コードの内容としては、まず CompletableFuture.supplyAsync(Supplier< Path>) からスタートします。

まず引数となる Supplier は、 T (ここでは Path )を返す get() を持つだけの interface です。

で、 supplyAsync は asyncSupplyStage(ASYNC_POOL, supplier) を呼びます。

この ASYNC_POOL は、 ForkJoinPool.getCommonPoolParallelism() が 1 より大きい場合は ForkJoinPool のインスタンスを返します。

また、AccessController.doPrivileged ~ により、 ForkJoinPool に付与されている権限で実行されます。

ForkJoinPool.java
~省略~
common = AccessController.doPrivileged(new PrivilegedAction< >() {
            public ForkJoinPool run() {
                return new ForkJoinPool((byte)0); }});
~省略~

getCommonPoolParallelism() ですが、これが 1 以下になる場合、というのはシングルコアの PC ということなのでしょうか。

ちなみに Surface Pro 6(Core i5-8250U) で実行したところ、 7 が返ってきました。

コア数とイコールでもないのが気になるところですが、今回はスキップすることにします。

ForkJoinPool クラスは AbstractExecutorService を継承しており、 Future パターンで登場した ExecutorService の役割をになっているようです。

asyncSupplyStage

さて asyncSupplyStage に進みます。

ここでは ForkJoinPool.execute に AsyncSupply< U>(ompletableFuture< U> d, Supplier< U> f) を渡しています( U は今回の場合 Path[] )。

ということで、 Callbale< String> にあたるクラスは AsyncSupply< U> ということなのだと思います。

次は ForkJoinPool.java で定義されている AsyncSupply を見てみます。

AsyncSupply

ForkJoinPool.java > AsyncSupply< T>
~省略~
public void run() {
    CompletableFuture< T> d; Supplier< ? extends T> f;
    if ((d = dep) != null && (f = fn) != null) {
        dep = null; fn = null;
        if (d.result == null) {
            try {
                d.completeValue(f.get());
            } catch (Throwable ex) {
                d.completeThrowable(ex);
            }
        }
        d.postComplete();
    }
}
~省略~

ここで気になる処理は 4 つです。

  1. CompletableFuture< T>.result
  2. CompletableFuture< T>.completeValue()
  3. Supplier< ? extends T>.get()
  4. CompletableFuture< T>.postComplete()

1.はここまで追ってきた処理には登場しなかったような気がします。

他で代入している値を見ると、複数の処理が行われた時に、先に実行されたタスクでエラーが発生した場合に、処理をスキップして終了する( 4.で )ためのもののようです。

2.は RESULT.compareAndSet(this, null, (t == null) ? NIL : t) ( t は Supplier< ? extends T>.get() )を返しています。

この RESULT が何かというと、このような内容になっています。
( try ~ catch などは省いています)

MethodHandles.Lookup l = MethodHandles.lookup();
VarHandle RESULT = l.findVarHandle(CompletableFuture.class, "result", Object.class);

これにより、 CompletableFuture にある volatile Object result; への参照が取得できます。

VarHandle は 動的に変数への強い型を持った参照を得るためのもので、ここでは CompletableFuture の result に対する参照を取得している、と。

なおここでアクセスチェックが行われるために、 AccessController.doPrivileged ~ が必要となるようです。

2.に戻ると、 compareAndSet によって Supplier< ? extends T>.get() の戻り値を result にセットしているようです。

ここは後でもう少し調べてみることにします。

最後 4.ですが、処理完了後に実行するために登録されている処理を、順に実行して外していく、ということを行っているようです。

が、ここだけに限らないのですが、 CompletableFuture をはじめ同じ型のインスタンスが複数登場して混乱してきているので、後日修正するかもしれません/(^o^)\

compareAndSet について

さて、ちょこちょこと登場している VarHandle.compareAndSet 。

これが何をしているのかちょっとだけ調べてみることにしました。

例えばこのようなクラスがあったとして。

VarHSample2.java

package SearchContainedFiles;

class VarHSample2{
    String message;
    void call(){
        System.out.println("VarHSample2 " + message);
    }
}

このようにすると、 VarHSample2.java の message の中身が変更されます。

VarHSample.java

package SearchContainedFiles;

import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;

class VarHSample {
    void call(){
        
        MethodHandles.Lookup l = MethodHandles.lookup();
        VarHandle handle;
        VarHSample2 sample2 = new VarHSample2();
        try{
            // 引数は 1. ターゲットとなるクラス 2. 変更したい変数名 3. 2.の型
            handle = l.findVarHandle(VarHSample2.class, "message", String.class);

            // 引数は 1. findVarHandle の第一引数のインスタンス 
            // 2. findVarHandle の第二引数の変数の処理実行前の値
            // 3. 2.の値が変数と等しかった場合に代入される値
            boolean result = handle.compareAndSet(sample2, null, "hello!");
            System.out.println(result);     // true
        }
        catch(IllegalAccessException e){
            System.out.println(e.getMessage());
        }
        catch(NoSuchFieldException e){
            System.out.println(e.getMessage());
        }
        sample2.call();    // 「VarHSample2 hello!」と出力される.
    }
}
  • findVarHandle で、ターゲットとなる変数は、呼び出し元である VarHSample.java からアクセス可能である必要があります(アクセス不可の場合、実行時にエラー)。
  • findVarHandle と compareAndSet の第一引数の型は同じである必要があり、違っていると実行時にエラーがでます。
  • compareAndSet の第二引数の値が、 findVarHandle の第二引数で指定する変数と異なる場合、戻り値は false となり、変数の値はそのままになります。

上記を踏まえてもう一度先ほどのコードを見てみると...

MethodHandles.Lookup l = MethodHandles.lookup();
VarHandle RESULT = l.findVarHandle(CompletableFuture.class, "result", Object.class);

// T は Supplier< ? extends T>.get() の戻り値.
RESULT.compareAndSet(this, null, (t == null) ? NIL : t)

ForkJoinPool.java > AsyncSupply< T> の completeValue 実行時に、 result が null で、かつ Supplier< ? extends T>.get() が null でなかった場合に T (今回は Path[] )が代入される、という動きになっているようです。

ここで一旦切りまして、次は get() で処理の完了を待ち受けてみたいと思います。

ASP.NET Core のアプリを Application Request Routing + IIS で動かしてみる

はじめに

相変わらずコロコロ話題が変わっていますが。。。お察しください。

タイトル通り、 ASP.NET Core で作ったアプリを IIS を使って動かしたい話です。

ASP.NET Core アプリの発行は以前挑戦しましたが、この時は Kestrel で動かしていただけでした。

ASP.NET Core アプリを IIS で実行する方法として、大きく 2 つあります。

  1. Application Request Routing を使う
  2. ASP.NET Core Module を使う

今回は Application Request Routing を試してみることにします。

Application Request Routing (以下 ARR)という IIS の Extension を使うことで、 IIS で指定した URL にアクセスがあった場合に、localhost:5000 で動かしている ASP.NET Core アプリにリダイレクトして表示することができるようになります(リバースプロキシ)。

f:id:mslGt:20190511021225p:plain

ASP.NET Core に組み込まれている Kestrel は、セキュリティなどの機能が IIS に比べて劣るとのことですが、この方法により外部からのアクセスは IIS で処理し、対象となる URL へのアクセスがあった場合のみ対応すれば良いことになります。

また ARR のインストール( + そのための Web Platform Installer (以下 Web PI )のインストール)は必要ですが、後述の self-contained application として発行していれば実行するサーバーマシンに .NET Core がインストールされていなくても動作させられたり、既存の環境をあまりいじらなくても良い、というのが利点だと思っています。

ASP.NET Core アプリの準備

ASP.NET Core アプリを作る

いつものように Empty テンプレートで ASP.NET Core プロジェクトを作成します。

で、 ASP.NET Core アプリ自体は localhost でのみアクセスできれば良いため、この状態で発行してしまいます。

self-contained application として発行する

self-contained application として発行することで、アプリを動作させるマシン上に .NET Core がインストールされていなくても動作させることが可能になります。

方法は .csproj ファイルに実行環境( Runtime )の情報を追加することと、発行時にそれを指定するだけです。

RunOnIisSample.csproj

< Project Sdk="Microsoft.NET.Sdk.Web">
    < PropertyGroup>
        < TargetFramework>netcoreapp2.2< /TargetFramework>
        < RuntimeIdentifiers>win-x86< /RuntimeIdentifiers>
    < /PropertyGroup>

    < ItemGroup>
        < PackageReference Include="Microsoft.AspNetCore.App" />
        < PackageReference Include="Microsoft.AspNetCore.Razor.Design" Version="2.2.0" PrivateAssets="All" />
    < /ItemGroup>

< /Project>
  • なおサンプルなどを見ると win10-x64 があるので win7-x86 としたらちゃんと動かなかったのも良い思い出(白目)。

発行

発行はいくつか方法があると思いますが、今回はコマンドで。

dotnet publish -c Release -r win-x86

これでプロジェクトの bin/Release/netcoreapp2.2/win-x86/publish 以下に、依存する dll ファイルとともに実行ファイルである RunOnIisSample.exe が出力されます。

IIS の準備

IIS Extension のインストール

IIS のインストール自体が完了していない場合は先にそちらを完了させてください。

まず ARR をインストールするのに必要な、 Web PI をインストールします。

インストール後、直接 Web PI を開くか、 IIS マネージャーから起動します。

検索で ARR を探し、インストールします( ver.3.0 を選びました)。

インストールが終わったら IIS マネージャーを再起動します(開いている場合)。

インストールした項目が追加されていれば完了です。

f:id:mslGt:20190511021319p:plain

IIS にアプリケーションを追加

まず URL 書き換えのベースとなる URL の登録を行います。

C:\inetpub\wwwroot に空のフォルダーを追加します。
※ 本来はアプリケーションのファイルを置くのですが、今回は URL を書き換えるためだけに用意するため、空で問題ありません。
(ただしもう少しちゃんとした方法はあるかも)

IIS マネージャーを起動し、左のツリーで サイト > Default Web Site を開きます
(最新の状態になっていれば、先ほどのフォルダーも表示されているはずです)。

Default Web Site 上で右クリック > アプリケーションの追加 から、アプリケーションを追加します。

f:id:mslGt:20190511021451p:plain

サイトの URL + エイリアス (今回は http://localhost:80/samplesite )でアクセスできるようになります。

物理パスは先ほどの空フォルダーを指定します。

f:id:mslGt:20190511021536j:plain

これで http://localhost:80/samplesite という URL が使用可能になるわけですが、中身は空であるためアクセスしてもエラーになります。

Proxy の有効化

左のツリーのトップ(今回は DESKTOP-3I579DO~ )をクリックして ARR を開き、右のメニューから Proxy > Server Proxy Settings... を開き、 Enable proxy にチェックを入れて有効化します。

余談ですが、下の Proxy Type で、 Use URL Rewrite to inspect incomming request にチェックを入れて Reverse proxy に URL を設定すると、そのサイトに登録されている全 URL が Reverse proxy で指定した URL に飛ばされます。

便利な場合もあるとは思いますが、今回は個別に設定したいためチェックは入れずに置いておきます(なぜ説明した)。

URL 書き換え

もう一度作成した samplesite をクリックし、 URL書き換え を開きます。

右のメニューから 規則の追加 を起動し、 受信規則と送信規則 > リバースプロキシ を選択します。

受信規則のホストとして、 ASP.NET Core アプリで指定している localhost:5000 を設定します。

f:id:mslGt:20190511021606j:plain

この状態で ASP.NET Core アプリを起動し、 http://localhost:80/samplesite にアクセスすると、 http://localhost:5000 を開いた時と同じものが表示されます。

今回 IISlocalhost でアクセスしているせいであまりありがたみは感じられないのですが、これで IIS を外部公開しさえすれば ASP.NET Core アプリを外部から表示することができるようになります。

Static File を読み込む

JavaScriptCSS などの Static File を wwwwroot に置き、 IIS を使ってページを開いたところ、正しく読み込めませんでした。

という話をしたいのですが、 http://localhost:80/samplesite で開いたところ読み込めてしまったため、先に hosts ファイルを使って localhost から masanori.example.jp というドメインに変更してみます。

hosts ファイルによるドメイン変更

C:\Windows\System32\drivers\etc にある hosts ファイルを管理者として開き、下記を追加します。

hosts

~省略~
{マシンのIPアドレス} masanori.example.jp
~省略~

ドメイン名はお好みで変更してください。

で、 IIS マネージャーで masanori.example.jp というサイトを作り、先ほどと同じ手順でアプリケーションを追加します。

f:id:mslGt:20190511021938j:plain

内容がかぶらなければ先ほどのアプリケーションは放置で問題ありませんが、使わないので消してもらってもよいと思います。

物理パスは同じフォルダーを指定してやれば、 URL の書き換え設定はそのまま有効になっていると思います。

http://masanori.example.jp:8088/samplesite2 のような URL で、 ASP.NET Core のページが表示されれば OK です。

Static Files のパスが合わない

(バグを再現する)準備が整ったところで、下記のファイルを追加していきます。

  • RunOnIisSample
    • Controllers
      • HomeController.cs
    • Views
      • Shared
        • _Layout.cshtml
      • _ViewStart.cshtml
      • Index.cshtml
    • wwwroot

HomeController.cs

using Microsoft.AspNetCore.Mvc;

namespace RunOnIisSample.Controllers
{
    public class HomeController: Controller
    {
        [Route("")]
        public IActionResult Index()
        {
            return View("/Views/Index.cshtml");
        }
    }
}

_Layout.cshtml

< !DOCTYPE html>
< html lang="ja">
< head>
    < meta charset="utf-8" />
    < title>@ViewData["title"]< /title>
    < link rel="stylesheet" type="text/css" href="src/css/site.css"/>
< /head>
< body>
    @RenderBody()
< script src="src/js/site.js">< /script>
< /body>
< /html>

_ViewStart.cshtml

@{
    Layout = "_Layout";
}

Index.cshtml

@{
    ViewData["title"] = "Home"; 
}

site.js

alert("hello");

site.css

body{
    background-color: aqua;
}

特筆すべきこともないというか、ほとんど空っぽの状態です。

localhost:5000 でページの色が変わること、アラートが表示されることは確認しておきます。

問題を確認

んで、 http://masanori.example.jp:8088/samplesite2 を開くと、 JavaScriptCSS も読み込まれていません。

Firefox の開発者ツールで確認したところ、 JavaScript について下記のようなエラーが出ていました。

http://masanori.example.jp:8088/src/js/site.js” からのスクリプトが読み込まれました。しかし、この MIME タイプ (“text/html”) は正しい JavaScriptMIME タイプではありません。[詳細] samplesite2
< script> のソース “http://masanori.example.jp:8088/src/js/site.js” の読み込みに失敗しました。

最初 MIME タイプの内容に気を取られてしまい、以前試したように Controller からファイルを返してみる、といったことも試していました。

が、原因は何のことはない、 URL が違うから、ということです orz

「samplesite2」の部分が抜けてしまっているために、 404 エラーが発生していました、と。

なぜ http://localhost:80/samplesite だと問題なく、 http://masanori.example.jp:8088/samplesite2 だとエラーになるのかは不明ですが、とにかく何とかしてみることにします。

IHostingEnvironment.EnvironmentName で切り分け

例えば実行環境が Development だった場合は ~/src/js/site.js を、それ以外は ~/samplesite2/src/js/site.js を開くようにします。

※根本的な解決ではない

HomeController.cs

using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;

namespace RunOnIisSample.Controllers
{
    private readonly string _additionalPath;

        public HomeController(IHostingEnvironment env)
        {
            _additionalPath = (env.EnvironmentName == "Development") ? "" : "samplesite2/";
        }
        
        [Route("")]
        public IActionResult Index()
        {
            ViewData["additional_path"] = _additionalPath;
            return View("/Views/Index.cshtml");
        }
    }
}
  • コンストラクターで ViewData に値を渡していないのは、そのタイミングで値を入れても View 側で受け取れなかったためです。読み込む前にリセットされるのかもしれません。

_Layout.cshtml

< !DOCTYPE html>
< html lang="ja">
< head>
    < meta charset="utf-8" />
    < title>@ViewData["title"]< /title>
    < link rel="stylesheet" type="text/css" href=@ViewData["additional_path"]"src/css/site.css"/>
< /head>
< body>
    @RenderBody()
< script src=@ViewData["additional_path"]"src/js/site.js">< /script>
< /body>
< /html>

一つ一つ設定していくのは面倒なので、一括で処理したいところではあるのですが。。。

次回に続く。。。かもしれない。

参照

IIS

ASP.NET Core

hosts

【Java】【C#】ローカルのファイルを開いて指定の文字列が含まれているか調べてみる

はじめに

諸事情により、指定したディレクトリ以下にあるファイルの中から、特定の文字列が含まれるファイルを調べたくなりました。

このような希望を叶えてくれるツールはきっとたくさんあると思いはするのですが、自分でやってみたかったので作ってみることにしました。

言語は Java で。理由は興味がある以外にはありません。

ただ、 Java でいきなり書き始めると書くのが久しぶりすぎて迷走しそうな気がしたので、まず C# で作ったあとできるだけ同じ動きとなるように Java で作ってみることにします。

あ、 OS はいつも通り Windows 10 です。

C# で書く

まずは C# で書いてみます。

環境は .NET Core ver.2.2.101 で、コンソールアプリとして作ります。

Main メソッドで async/await を使いたいので C# 7.1 にしておきますよ。

SearchContainedFiles.csproj

< Project Sdk="Microsoft.NET.Sdk">
  < PropertyGroup>
    < OutputType>Exe< /OutputType>
    < TargetFramework>netcoreapp2.2< /TargetFramework>
    < LangVersion>7.1< /LangVersion>
  < /PropertyGroup>
< /Project>

Program.cs

using System;
using FileLoaders;
using System.Threading.Tasks;

namespace SearchContainedFiles
{
    class Program {
        static async Task Main(string[] args) {
            if(args == null || args.Length < 2){
                Console.WriteLine("Need two args");
                return;
            }
            foreach(string file in (await FileLoader.SearchAsync(new Uri(args[0]), args[1]))){
                Console.WriteLine("File: " + file);
            }
        }
    }
}

FileLoader.cs

using System;
using System.IO;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace FileLoaders
{
    public class FileLoader{
        public static async Task< List< string>> SearchAsync(Uri rootDirectoryPath, string keyword){
            if(Directory.Exists(rootDirectoryPath.AbsolutePath) == false){
                return new List< string>();
            }
            List< string> files = new List< string>();            
            foreach(string file in Directory.GetFiles(rootDirectoryPath.AbsolutePath, 
                    "*", SearchOption.AllDirectories)){
                if(await CheckKeywordExistAsync(file, keyword)){
                    files.Add(file);
                }
            }
            return files;
        }
        private static async Task< bool> CheckKeywordExistAsync(string filePath, string keyword){
            using(StreamReader stream = new StreamReader(filePath)){
                while(stream.Peek() >= 0){
                    if(stream.Peek() < 0){
                        break;
                    }
                    if((await stream.ReadLineAsync()).Contains(keyword)){
                        return true;
                    }
                }
            }
            return false;
        }
    }
}

コマンドライン引数で渡されたパス以下のファイルパスを一括取得 -> 1つずつ開いて確認しています。

Java で書く

次は Java です。

環境は下記の通り。

  • OpenJDK ver.12.0.1
  • Gradle ver.5.4.1

Gradle は gradle init でプロジェクト作るとかビルドとか実行とかに使ってます。

こちらもコマンドラインアプリで。

Files.lines でファイルを読む(失敗)

ネットの情報などを頼りにまずは書いてみることにします。

App.java

package SearchContainedFiles;

import java.nio.file.Path;
import java.nio.file.Paths;

import SearchContainedFiles.FileLoaders.FileLoader;

public class App {
    public static void main(String[] args) {
        if(args == null || args.length < 2){
            System.out.println("Need two args");
            return;
        }
        for(Path file: FileLoader.Search(Paths.get(args[0]), args[1])){
            System.out.println(file.toString());
        }
    }
}

FileLoader.java

package SearchContainedFiles.FileLoaders;

import java.io.IOException;
import java.nio.file.FileVisitOption;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.util.stream.Stream;

public class FileLoader{
    public static Path[] Search(Path rootDirectory, String keyword) {
        if(Files.notExists(rootDirectory, LinkOption.NOFOLLOW_LINKS)){
            return new Path[0];
        }
        try(Stream< Path> files = Files.walk(rootDirectory,FileVisitOption.FOLLOW_LINKS)
            .filter(f -> f.getFileName().toString().matches("..*[.][a-zA-Z]+$"))){

           return files.filter(f -> CheckKeywordExistAsync(f, keyword))
            .toArray(Path[]::new);
        }
        catch(IOException e){
            System.out.println(e.getMessage());
        }
        return new Path[0];
    }
    private static boolean CheckKeywordExist(Path filePath, String keyword){
        try{
            return Files.lines(filePath)
                .anyMatch(p -> p.contains(keyword));
        }
        catch(IOException e){
            System.out.println(e.getMessage());
            return false;
        }
    }
}
  • Files.walk は指定のパス以下のフォルダ・ファイルの Path を一括で取得します。ファイルだけに絞りたかったため、正規表現でフィルタリングしてみました(後述しますが、素直にディレクトリかどうかを確認した方が良さそうです)。
  • C# の async/await 相当の処理ができないかどうかはいったん後回しにしています。

そしてエラー

これを実行すると、 Files.lines(filePath) の部分で MalformedInputException が発生しました。

Exception in thread "main" java.io.UncheckedIOException: java.nio.charset.MalformedInputException: Input length = 1

開こうとしたファイルの文字コードが違っていると。

BufferedReader でファイルを読む 1(失敗)

FileLoader.java

package SearchContainedFiles.FileLoaders;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.nio.charset.Charset;
import java.nio.file.FileVisitOption;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.util.stream.Stream;
import java.io.InputStreamReader;

~省略~
    private static boolean CheckKeywordExistAsync(Path filePath, String keyword){
        try(InputStreamReader streamReader = new InputStreamReader(
            new FileInputStream(filePath.toString()))){
                try(BufferedReader reader = Files.newBufferedReader(filePath, Charset.forName(streamReader.getEncoding()))){
                    return reader.lines()
                        .anyMatch(p -> p.contains(keyword));
                }
                catch(IOException e){
                    System.out.println(e.getMessage());
                    return false;
                }
        }
        catch(FileNotFoundException ex){
            System.out.println(ex.getMessage());
            return false;
        }
        catch(IOException ex){
            System.out.println(ex.getMessage());
            return false;
        }
    }
}

何か色々見失いつつあるような気がしますが。。。

エンコードが違っているのが原因であれば、ファイルのエンコーディングを取得してセットしてやれば良いのでは?

と思ったのですが、結果は特に変わりませんでした。

なお、エラーが起きるファイルのエンコーディングは MS932 でした。

ぐぬぬ。。。

getEncoding ではなく直接 Charset.forName("MS932") と指定した場合も結果は変わりませんでした。

なんでや。。。

BufferedReader でファイルを読む 2(成功)

結局 Files.~ を使うのをやめて new BufferedReader としたところ、問題なく読み込めるようになりました。

しかもエンコーディング指定なしで。

FileLoader.java

~省略~
    private static boolean CheckKeywordExistAsync(Path filePath, String keyword){
       
        try(BufferedReader reader = new BufferedReader(
            new InputStreamReader(
                new FileInputStream(filePath.toString())))){
                    return reader.lines()
                        .anyMatch(p -> p.contains(keyword));
        }
        catch(IOException ex){
            System.out.println(ex.getMessage());
            return false;
        }        
    }
~省略~

問題が解決したのは良いですが、なんとも釈然としない。。。(´・ω・`)

犯人はヤ……俺

ググった限り、別に MS932 のファイルを読むことができない、という話ではなさそうです。

ではなぜ?と思い、ファイルを一つずつ見ていったところ、問題が発生するのは .doc や .zip のような、テキストエディターでは開けない(文字化けしたような文字の羅列として表示される)ファイルでした。

あまり何も考えずにパス指定してそこにあるファイルを読み込む、といったことをしていたために、テキストとして扱えないファイルが混ざってエラーになっていた、と。

では逆にエラーにならずに読み込めたこの三つのクラス、

役割としては、まず画像データなどの raw データをストリームに変換できる FileInputStream がデータを読み込み、 InputStreamReader が文字型入力ストリームに変換 -> BufferedReader で文字列データを読み込む、ということのようです。

raw データから文字型入力ストリームに変換せずに、文字型でないデータを読み込んでしまうとここまで見てきたような問題が発生する、ということのようです。

.doc や .zip ファイルは、一行ずつ読み込めたとして検索できないような気がするのですが、変換なしで文字型入力ストリームとして扱えるかを確認する方法はあるのでしょうか。。。?
(今回調べた限りだと、実際に読み込んでみてエラーになるかを確認するか、拡張子を指定するぐらいしか思いつきませんでした)

というところまでを考えると、確実にテキストファイル(として扱えるファイル)のみを読み込む場合を除き、FileInputStream 、 InputStreamReader 、 BufferedReader を使うのが良いような気はしました。

まぁ今回は外部のライブラリ、フレームワークなどを( Gradle 以外)極力使わないようにしたのですが、それらを含めるともう少し選択肢もありそうですね。

いったん切ります。

参照

Java

Gradle

【ASP.NET Core】【TypeScript】PDF のダウンロード・アップロード 2

はじめに

平成 <--> 令和 をまたいでしまいましたが、続きです。
(令和になりましても引き続きよろしくお願いいたします (..)_ )

FormData を使ってファイル送信する場合の課題として、ファイルの容量制限が、少なくともデフォルトだとかなり厳しいことが挙げられます。

テキストファイル程度なら良いのですが、動画などになってくると数十 MB を超えたりします。

これをそのまま FormData として Body に入れて渡そうとすると BadHttpRequestException が発生します。

info: Microsoft.AspNetCore.Server.Kestrel[17]
      Connection id "XXXXXXXXXXXXX" bad request data: "Request body too large."
Microsoft.AspNetCore.Server.Kestrel.Core.BadHttpRequestException: Request body too large.
   at Microsoft.AspNetCore.Server.Kestrel.Core.BadHttpRequestException.Throw(RequestRejectionReason reason)
   at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.Http1MessageBody.ForContentLength.OnReadStarting()
   at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.MessageBody.TryStart()
   at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.MessageBody.ConsumeAsync()
   at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.ProcessRequests[TContext](IHttpApplication`1 application)
   at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.ProcessRequestsAsync[TContext](IHttpApplication`1 application)

Microsoft Docs によれば容量が大きいファイルのアップロードには stream を使う、という話があります。

ということで、これを試してみることにします。

Stream を使って PDF をアップロードする(失敗)

まぁ失敗というかなんというか。

サンプルでは Razor が使われていて、 View と Controller (コードビハインド?) で直接データのやり取りができると。

そのため Body に格納して渡すのではなく、 MemoryStream で直接メモリーに対象のファイルデータを読み込んでいるようでした。

まぁ素直に Razor 使えばよいのですが、今回は View 部分を完全に切り離せるようにしたいため、別の方法を探ります。

で、その方法って?

で、その方法なのですが、そもそも C# (Controller) 側が空の状態であっても、 Request の Body に格納して送信した時点で先ほどの Exception が発生するため、 View 側で何とかする必要がありそうです。

結局どうしたか

あれこれ調べてみたのですが、結局のところはファイルを分割して送信 -> C# (Controller) 側でひとまとめにする、という処理が必要になるようでした。

ということでこの方法を試してみます。

ファイルを分割してアップロード

ファイルの分割

ファイルを指定のサイズで分割する方法の一つに、 FileReader の readAsArrayBuffer を使うものがあります。

readAsArrayBuffer でファイルの内容を ArrayBuffer として読み込み、それを指定サイズ(バイト)ごとに分割して送信します。

mainPage.ts

exports.uploadFile = async function uploadFile(){
    const fileField = document.getElementById("upload_file_form") as HTMLInputElement;

    if(fileField === null ||
        fileField.files === null ||
        fileField.files.length <= 0){
        return;
    }
    // 本当は保存先のディレクトリ作成とか必要だと思います.

    const reader = new FileReader();
    let buffer: Uint8Array;

    // 約 8MB (実際は 7.62MB でした)ずつ分割.
    const size = 8000000;

    // 送信は前回と同じく FormData に.
    let formData: FormData;
    const file = fileField.files[0];

    // 内容が読み込まれたらファイル送信開始.
    reader.onload = async function(e: ProgressEvent) {
        
        // 参考リンクでは e.target.result になっているものがありますが,
        // 見つからなかったので FileReader から取得.
        const result = reader.result;
        if(result === null){
            console.error("result is null");
            return;
        }
        // 読み込んだ内容から Uint8Array を作り、指定サイズごとに分割 -> 送信.
        buffer = new Uint8Array(result as ArrayBuffer);
        let failed = false;   
        let fileIndex = 0;

        for(let i = 0; i < buffer.length; i += size){
            failed = false;
            formData = new FormData();
            formData.append('file', new Blob([buffer.subarray(i, i+size)]))
            await fetch('files/pdf', {
                method: 'POST',
                body: formData,
                headers: {
                    "FileName": file.name,
                    'FileIndex': fileIndex.toString(),
                }
            })
            .catch(error => {
                console.error('Error:', error);
                failed = true;
            });
            if(failed){
                console.log('failed');
                return;
            }
            fileIndex = fileIndex + 1;
            
            // このタイミングでプログレスバーをいじったりすると良さそうです.
        }

        // ファイルを送信し終わったらひとまとめにする処理を実行.
        var endFormData = new FormData();
        endFormData.append('fileName', file.name);
        await fetch('files/pdf/finished', {
            method: 'POST',
            body: endFormData,
        })
        .then(response => console.log('ok'))
        .catch(error => console.error('Error:', error));
    }
    // ファイルの内容を読みこむ.
    reader.readAsArrayBuffer(file);
}
  • 最初 PDF で試していたため URL が PDF になっていますが、手元にあるファイルサイズの関係で PDF 以外のファイルも使っていますがお気になさらず。。。

Uint8Array について

new Uint8Array(result as ArrayBuffer) のような形で生成している Uint8Array 。

見た目的には C# の List のように、中の値をコピーして新しい配列を作っているのかな? とも思ったのですが、↓ Uint8Array.prototype.buffer を見ると元の ArrayBuffer への参照を保持しているようにも思えます。

この引数について、ファイルサイズが正しい場合中身が間違っていてもエラーが発生せずに 分割 -> 送信 と処理が進んでしまうため、マージ完了後に初めてファイルが正しくないことに気づく、という落とし穴があるため注意が必要です。
(参考にしたサイトで使われていた ProgressEvent.target.result が見つからなかったのですが、代替となるデータを渡し間違えてえらい苦労をしましたorz )

Header について

fileField.files[0] をそのまま渡していた場合と異なり、ArrayBuffer として分割したデータからはファイル名が取得できません。

そのため何らかの方法で送信する必要があります。

また分割データを順番通り復元するには、送信したデータの番号も送ってやる必要があります。

Body の FormData に追加することもできますが、今回は Header に入れることにしました。

理由は試したときに Body に複数データをセットした場合のサーバー側での受け取り方がわからなかったのと、エンティティヘッダーでは「メッセージボディの内容を記述する」とあったためです。

分割されたデータの受け取り

サーバー側です。

前回と同じく引数を IFormFile にして受け取ることも可能です。

が、 Microsoft Docs も参考にカスタムクラスで受け取ってみることにしました。

UploadFile.cs

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;

public class UploadFile{
    [FromForm(Name="file")]
    public IFormFile File{get; set;}
}

HomeController.cs

using System.Collections.Generic;
using System.Text;
using System;
using System.IO;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.FileProviders;
using Microsoft.Net.Http.Headers;
using System.Net.Http;
using System.Linq;

namespace Controllers {

    public class HomeController : Controller {
        private static readonly string TmpLargeFileDirectory = @"C:/Users/XXX/workspace/DownloadUploadSample/files/tmp/";

        [HttpPost]
        [Route ("/files/pdf")]
        public async Task< IActionResult> UploadPdfFile (UploadFile uploadFile) {
            
            string fileName = Request.Headers["FileIndex"] + "_" + Request.Headers["FileName"];
            if(uploadFile?.File == null){
                Console.WriteLine("failed");
                return BadRequest();
            }
            
            using(var stream = new FileStream(TmpLargeFileDirectory + fileName, FileMode.Create)){
                await uploadFile.File.CopyToAsync(stream);
            }
            return Ok();
        }
    }
}

ほぼ前回と同じです。

カスタムのクラスを使うことで、複数データを渡す場合も名前指定で受け取ることができます。

今回は端折ってますが、ファイルアップロードの開始時点でファイルの保存先を作る、同時に別のユーザーがアップロードしたときに混ざらないようにするなどの処理が実際には必要になります。

Request.Body

データの受け取りについて、 fetch では Body にセットしているのだから、 Request.Body から受け取れないかな~と思ってみたのですが、データをうまく変換できず、断念しました。

ファイルをひとまとめにする

最後に出力されたファイルを一つにまとめます。

ローカルのファイルを読み込む・書き出す辺りは以前ローカルのファイルを読み込んだときの流用です。

HomeController.cs

~省略~
[HttpPost]
[Route ("/files/pdf/finished")]
public async Task< bool> FinishUploading(string fileName){

    List< byte> readBytes = new List< byte>();
    using(PhysicalFileProvider provider = new PhysicalFileProvider(TmpLargeFileDirectory)){
        foreach(IFileInfo fileInfo in provider.GetDirectoryContents(string.Empty)
            .Where(f => f.IsDirectory == false)
            .OrderBy(f => {
                string[] fileNames = f.Name.Split('_');
                int.TryParse(fileNames[0], out int index);
                return index;
            })){
            using(Stream reader = fileInfo.CreateReadStream()){
                int fileLength = (int)fileInfo.Length;
                byte[] newReadBytes = new byte[fileLength];
                reader.Read(newReadBytes, 0, fileLength);
                readBytes.AddRange(newReadBytes);
            }
        }
    }
    using (FileStream stream = new FileStream(TmpLargeFileDirectory + fileName, FileMode.Create)) {
        await stream.WriteAsync(readBytes.ToArray(), 0, readBytes.Count);
    }
    // 本当はこの後分割保存したファイルを削除した方が良いと思います.
    return true;
}
~省略~

おわりに

一応これでサイズが大きなファイルでもアップロードできるようにはなりました。

ただ、ファイルを分割 -> 保存 してもう一度読み込み -> マージして保存 とするのは無駄が大きいように思います。

下記は Rails + nginx ではありますが、最終 Rails ではなく nginx の機能を使って速度を上げています。

これを考えると、もう少し別の方法も考慮した方が良いように思いました。

冒頭でスキップしてしまいましたが、せっかくの ASP.NET Core なので、 Razor を使ってみるのも良いかもしれませんね。
(本当は今回試すつもりでしたが、分割アップロードで躓いて心が折れました。という言い訳)

【ASP.NET Core】【TypeScript】PDF のダウンロード・アップロード 1

はじめに

(いつものごとく) とある事情で PDF をダウンロード・アップロードする機能が欲しかったため、試してみたら苦労した、という話です。

以前 ASP.NET Core でローカルの特定のディレクトリにあるファイルをアクセス可能にしたことがありましたが、今回も同様に、特定のディレクトリに PDF ファイルを置き、それをダウンロードする機能と、そのディレクトリにファイルをアップロードする機能を作ってみます。

exports が見つからない

のっけから話の腰を折り曲げるわけですが。

今回の検証用に ASP.NET Core プロジェクトを作り、 npm install で TypeScript や webpack などを追加したところ、以前 webpack でグローバル関数を HTML からみられるようにするために使っていた 「 exports.greeting() = async function greeting(){ ~ } 」の exports が見つからないというエラーに遭遇しました。

結局のところ、 @types/node をインストールすることで解決しました。

npm install --save @types/node

型が無いからエラーに、というのはわかるのですが、これまではなぜエラーになっていなかったのか。。。

準備

まずは準備から。

PDF ファイルはプロジェクトと同じ場所に置いておきます。

で、 Static file として公開します。

Startup.cs

using System.IO;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileProviders;

namespace DownloadUploadSample
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();
        }
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            app.UseStaticFiles();
            
            app.UseStaticFiles(new StaticFileOptions {
                FileProvider = new PhysicalFileProvider(
                    Path.Combine(@"C:\Users\example\OneDrive\Documents\workspace\DownloadUploadSample", "files")),
                RequestPath = "/files"
            });
            
            app.UseMvc();
        }
    }
}

HomeController.cs

using Microsoft.AspNetCore.Mvc;

public class HomeController: Controller{
    [Route("/")]
    public IActionResult Index(){
        return View("./Views/Index.cshtml");
    }
}

ダウンロードもアップロードも Index.cshtml から実行することにします(個別で作るの面倒なので)。

PDF をダウンロードする

まずはファイルに対する Link を設定して、ダウンロードできるか試してみます。

Index.cshtml

< a href='http://localhost:5000/files/sample.pdf' target="_brank">PDF< /a>

結果としては、ダウンロードされるのではなくブラウザの PDF Viewer で表示されました。

一応 target="_brank" も外してみましたが、単に開いているタブで表示されるだけで、ダウンロードされることはありませんでした。

download 属性を追加する

モダンなブラウザであれば、下記のように download 属性をつけることで、強制的にダウンロードさせることができます。

Index.cshtml

< a href='http://localhost:5000/files/sample.pdf' download>PDF< /a>

めでたしめでたしといいたいところですが、この属性、 IE だと( ver.11 でも)無視されます/(^o^)\

またか。。。

※なぜか家の IE 11 で確認したところ問題なくダウンロードのポップアップが表示されました。。。

なんでや。。。orz

でも話が終わってしまうので、できなかった体で進めます。

msSaveOrOpenBlob

IE 、 Edge で使うことのできる、 msSaveOrOpenBlob を利用してダウンロードする方法も試してみました。

ほぼ上記のコピペではあったものの、ファイルがダウンロードできることが確認できました。

なぜこれを選ばなかったかというと、下記 2 つの理由によります。

  1. IE 、 Edge でしか使う必要がない (しかも Edge は download 属性に対応しているためこの用途では不要)
  2. 「ダウンロードして開く」「保存する」のポップアップがファイルダウンロード後に表示される

2についてはリンククリック時にポップアップを出して、スピナーなどを出して。。。という機能を用意すれば良いのですが、 UI 的にこだわりたい場合はともかく、ただダウンロードできれば良いレベルの場合も、というのはちょっとツラいですね。

Web ブラウザーが表示対応しているばかりにこんなことに。。。(いつもは便利に使わせていただいております (..)_ )

ASP.NET Core の機能でダウンロード

いったん JavaScript で何とかするのをあきらめて、今回は ASP.NET Core 側で何とかしてみることにしました。

HomeController.cs

~省略~
[Route("/files/pdf")]
public IActionResult DownloadPdfFile(){
    return DownloadFile(@"C:\Users\example\OneDrive\Documents\workspace\DownloadUploadSample\files", "sample.pdf");
}
private FileResult DownloadFile(string filePath, string fileName)
{
    IFileProvider provider = new PhysicalFileProvider(filePath);
    IFileInfo fileInfo = provider.GetFileInfo(fileName);
    Stream readStream = fileInfo.CreateReadStream();
    string mimeType = "application/pdf";
    return File(readStream, mimeType, fileName);
}
~省略~

ファイルを Stream に変換し、 FileResult として返すことで PDF として扱われなくなる、ということなのでしょうか。

やっぱりやりたいことに対して大げさすぎる気がしないでもないですが、ともあれ PDF がダウンロードできるようになりました。

FormData を使って PDF をアップロードする

ASP.NET Core のドキュメントを見ると、ファイルのアップロードには フォームデータとしてアップロードする方法と、 Stream でアップロードする方法があるとのこと。

あまりファイル容量が大きい場合は向かないとのことですが、まずはフォームデータを試してみたいと思います。

Index.cshtml

~省略~
< input type='file' id='upload_file_form'>
< button onclick='Page.uploadFile()' accept='pdf'>send< /button>
< script src='./src/js/main.bundle.js'>< /script>

mainPage.ts

exports.uploadFile = async function uploadFile(){
    // HTMLInputElement.files で 参照ボタンを押して選択されたファイルが取得できる.
    const fileField = document.getElementById("upload_file_form") as HTMLInputElement;

    if(fileField === null ||
        fileField.files === null ||
            fileField.files.length <= 0){
        return;
    }
    // フォームに入れて fetch で送る.
    const formData = new FormData();
    formData.append('file', fileField.files[0]);

    await fetch('files/pdf', {
        method: 'POST',
        body: formData
    })
    .then(response => response.json())
    .catch(error => console.error('Error:', error))
    .then(response => console.log('Success:', JSON.stringify(response)));
}

HomeController.cs

~省略~
[HttpPost]
[Route("/files/pdf")]
public async Task UploadPdfFile(IFormFile file){
    if(file == null){
        return false;
    }
    // 受け取ったファイルを FileStream に変換し、ファイル名を変更して保存.
    using(var stream = new FileStream(
            @"C:\Users\example\OneDrive\Documents\workspace\DownloadUploadSample\files\" +
                DateTime.Now.ToString("yyyyMMddHHmmss") + 
                    file.FileName, FileMode.Create)){
        await file.CopyToAsync(stream);        
    }

    return true;
}
~省略~

なぜか fetch の option として、 headers: { "Content-Type": "application/pdf" } を渡すと IFormFile が null になってしまうなど、しっくり来ていないところも多いですが、とにかくファイルを送信することはできました。

ただ、ファイルの容量が数十 MB など大きくなってくると、 ASP.NET Core 側でエラーが発生してしまいます。

ググったところ、この容量制限を緩和することはできるようですが、 ASP.NET Core のドキュメントでも挙げられている通り Stream を使うのが良さそうです。

ということで、次回は Stream を使ったアップロードの話。。。の予定。

コンパイルを自動化したい話 + (Pikaday + TypeScript) で IE でも DatePicker を表示したい話

はじめに

ここまで tsc による TypeScript -> JavaScript 変換、 webpack によるバンドルや PostCSS -> CSS 変換など、ツールによるコンパイルをいくつか試してきたわけですが、正直これを手動でやり続けるのは大変です。

ということで、これを自動で実行してくれるようにしてほしかった話と、前回の DatePicker の話の続きです。

npm scripts で自動的にコンパイル

npm scripts を使って面倒なコンパイル処理をもう少し簡素化できるようです。

npm scripts 以外にも自動で処理を実行してくれるツールは色々あるようですが、全く別のツールを導入しなくて良い、ということと、今回やりたいことは十分実現できそう、ということで今回はこれでいくことにしました。

とりあえず npm scripts でコンパイルを実行

まずはこれまで実行してきた下記二つのコマンドを npm scripts で実行してみます。

  • npx webpack
  • npx postcss pcss/*.css -c postcss.config.js -d src

これらを package.json に scripts として追加します。

package.json

{
    
    "scripts": {
        "css": "npx postcss pcss/*.css -c postcss.config.js -d src",
        "webpack": "npx webpack"
    },
    
    "devDependencies": {
        ~省略~
    }
}

で、あとはターミナルから下記のように実行すれば OK です。

npm run css

webpack の方は

npm run webpack

まとめて実行

今は二つですが、今後増えてくるとやっぱり面倒なので、一括で実行したいところ。

npm-run-all を使ってみます。

npm install --save-dev npm-run-all 

package.json

{
    "scripts": {
        
        "all": "run-s css webpack",
        
        "css": "npx postcss pcss/*.css -c postcss.config.js -d src",
        "webpack": "npx webpack"
    },
    "devDependencies": {
        ~省略~
    }
}

これで npm run all とすれば両方実行してくれます。

自動で実行してほしい

2 回実行するものが 1 つにまとまって、少しは楽になったもののやっぱり変更のたびに都度実行するのはちょっと。。。

Rider で tscコンパイルを、ファイルの変更を感知して自動で実行してくれたように、 webpack や PostCSS も自動実行してほしいですね。

通常 npm scripts で実現するためには、 watch を使うことができます。

npm install --save-dev watch

ですが、 webpack 、 PostCSS の場合、 watch を使わなくてもオプションとして watch の機能を持っています。

ということで、 package.json を下記のように変更するだけで、ファイルの変更の監視、コンパイルの自動実行をしてくれるのでした。

package.json

{
    "scripts": {
        "all": "run-s css webpack",
        "css": "npx postcss pcss/*.css -c postcss.config.js -d src -w",
        "webpack": "npx webpack -w"
    },
    "devDependencies": {
        ~省略~
    }
}

別でターミナルを立ち上げて実行

今回の開発は Visual Studio Code (以下 VS Code )を使っているのですが、 VS Code ではメニューからターミナル( PowerShell )を立ち上げることができます。

コンパイルや npm install をする場合はこれを使うのが便利なのですが、上記の処理については別で PowerShell などを立ち上げて実行するのが良いかと思います。

理由は明示的に処理を終了するまでずーっと監視し続けるため、他のコマンドが打てなくなるためです。

IE11 での input type='date'

前回の話で、 DatePicker を表示するのは input の type を date にするだけ。

えらい簡単だな~凄いな~と思っていたわけですよ。

IE で確認するまでは。

まぁ当然のように動かない、と/(^o^)\

自分で作るのは厳しそうなので、良さげなものないかな~と思っていたところ、 Pikaday を見つけました。

シンプルに使えそうとか良いところは色々あるわけですが、とりわけ IE の対応が ver.7 からというのがありがたい (..)_ (今回は IE7 には対応していませんが)

ということで試してみます。

準備

とにかくインストールから。

色々方法はあるようですが、 npm install することにしました。

npm install --save-dev pikaday

CSS の読み込み

Pikaday を使うにあたり、必要なファイルは 2 つです。

  • Pikaday.js
  • Pikaday.css

JavaScript の方は import なりなんなりすれば利用できるので問題ありません。

ただ、 CSS をどうするか。

ファイル自体は node_modules > pikaday > css に置かれています。

HTML から node_modules 以下のパスを指定して読み込む、または手動あるいは npm scripts でコピーという手もなくはないですが、自分で設定する CSS ファイル程更新は頻繁ではないでしょうし、かといって放置というわけにもいかずいまいち。

などと思っていたら、 postcss-import を使う方法が見つかりました。

これを使うことで、 JavaScript の import / export で分割したファイルを webpack でひとまとめにするのと同じようなことができます。

postcss-import を使う

上記リンク参照というところではありますが、一応ここにも書き残しておきます。

まずはインストール。

npm install --save-dev postcss-import

PostCSS から使用するので、 postcss.config.js に追加します。

postcss.config.js

module.exports = {
    plugins: [
        
      require('postcss-import')(),
      
      require('autoprefixer')({
        "grid": true,
        "browsers": [
          "last 2 versions"
        ]
      }),
      require('precss'),
    ]
  }

で、表示するページで使用する CSS に import を追加してやります。

page.css

@import '../node_modules/pikaday/css/pikaday.css';

~省略~

Pikaday の README と違ってパスが ../ になっているのは、 page.css が pcss というディレクトリ内にあるからです(想定されている CSS ファイルとパスが異なる)。

これでコンパイルすると、 page.css の内容に pikaday.css が追加されたものが出力されます。

HTML から node_modules 以下のパスを指定する場合、 ASP.NET Core (というか Web アプリ側)の静的ファイルの公開範囲をいじる必要が出てきますが、 PostCSS のコンパイル時だけ参照するのであれば問題はぐっと減ってくれそうです。

なお、何らかの理由で pikaday.css の内容と他のファイルを分けたい場合は、 @import ~ だけ書いた

Pikaday を使う

Pikaday を使う場合、対象の要素を指定する、日付のフォーマットを指定する、といった初期化処理を行う必要があります。

< script src="pikaday.js">< /script>
< script>
    var picker = new Pikaday({ 
        field: document.getElementById('datepicker') 
    });
< /script>

注意点としては document.getElementById('datepicker') が実行されるより前に該当の DOM 要素がロードされている必要があるため、この処理は HTML の下部に書く必要がある、ということです。

フォーマットを変更する

デフォルトだと下記のデモのように曜日や月が英語で表示されます。

例えばこれを日本語に変更したい場合、初期化処理時に指定することができます( 項目は pikaday.js の defaults で初期値が設定されています)。

で、これらを DatePicker 使うたびに設定するのはツラすぎるので、クラスにまとめてみました。

import Pikaday from 'pikaday';

class CustomDatePicker{
    public static getPikaday(elementName: string): Pikaday{
        const today = new Date();
        const yearFrom = 1999;
        const yearTo = today.getFullYear() + 1;

        return new Pikaday({
            field: document.getElementById(elementName),
            firstDay: 1, // 月曜始まりにする
            minDate: new Date(yearFrom.toString() + '-1-1'), // 選択できる最小範囲
            maxDate: new Date(yearTo + '-12-31'),
            yearRange: [yearFrom, yearTo], // 選択できる年の範囲。 1999 - 一年後までとする
            yearSuffix: '年', // 2019年 と表示されるようにする
            showMonthAfterYear: true, // 2019年 四月 と表示されるようにする
            i18n: {
                previousMonth : '前月',
                nextMonth     : '次月',
                months        : ['1月','2月','3月','4月','5月','6月','7月','8月','9月','10月','11月','12月'],
                weekdays      : ['日曜','月曜','火曜','水曜','木曜','金曜','土曜'],
                weekdaysShort : ['日','月','火','水','木','金','土'],
            }
        })
    }
}
export {CustomDatePicker}

Pikaday を import する

@types/pikaday のインストールが必要そうです。

npm install --save-dev @types/pikaday

field で渡せる要素

getElementById となっている通り、一つの Pikaday インスタンスで設定できる要素は一つだけのようです。

ということで、 ID は外から渡せるようにしています。

複数ファイルをバンドルして出力する

例えば今回の CustomDatePicker を ページのスクリプトとは別にバンドルしたい場合。

webpack.config.js の entry で複数パスを渡してやることと、 output で名前が重複しないよう名前を指定する必要があります。

webpack.config.js

var path = require('path');

module.exports = {
    mode: 'development',

    entry: {
        'main':'./ts/mainPage.ts',
        'datePicker':'./ts/customDatePicker.ts',
    },
    
    module: {
        rules: [
            {
                test: /\.tsx?$/,
                use: 'ts-loader',
                exclude: /node_modules/
            }
        ]
    },
    resolve: {
        extensions: [ '.tsx', '.ts', '.js' ]
    },
    output: {
        filename: '[name].bundle.js',
        path: path.resolve(__dirname, 'src/js'),
        library: 'Page',
        libraryTarget: 'umd'
    }
};

課題としては、(今回使っていませんが) SourceMap を生成すると一つ分しか生成されない、ということと、 npm run all で TypeScript を変更した場合に処理がうまく動いていない気がする、という二点は把握しています。

この辺りは解決したら追記、または別記事で投稿します。

flexbox でレイアウトを組みたい話

はじめに

とりあえずベンダープレフィックスの件は解決できました(多分)。

ということで、 flexbox を使ってレイアウトを組んでみたいと思います。

まずはレスポンシブデザインにはしないことにして、かつベンダープレフィックス無しでの指定に対応している・個人的に一番よく使っている Firefox をベースに作ってみたいと思います。

Autoprefixer や IE は?というところですが、まずはレイアウトを組んで、そのあと対応としたいと思います。
(その方法が良いのかどうかはわかりませんが)

外枠を作る

まず各エリアの外枠を作ってみることにします。

mainPage.html

< !DOCTYPE html>
< html lang="jp">
    < head>
        < meta charset="utf-8">
        < title>main
        < link rel="stylesheet" type="text/css" href="../src/page.css" >
    < /head>
    < body>
        < header id="global_header">
        < /header>
        < section id='page_title'>page title< /section>
        < div id='container'>
            < section id='search_area'>
            < /section>
            < section id='item_view'>
                < div id='search_result'>
                < /div>
                < div id='image_view'>
                < /div>
            < /section>  
        < /div>
    < /body>
< /html>

page.css

html{
  height: 100%;
  width: 100%;
  margin: 0px;
}
body{
  height: 100%;
  width: 100%;
  margin: 0px;
  min-width: 900px;
  min-height: 600px;
}
header{
  width: 100%;
  height: 5%;
  background-color: cornflowerblue;
}
#page_title{
  width: 100%;
  height: 5%;
  background-color: aqua;
}
#container {
  display: flex;
  flex-direction: row;
  width: 100%;
  height: 90%;
  background-color: gray;
}
#search_area{
  background-color: pink;
  flex: 1;
}
#item_view{
  flex: 3;
}
#search_result{
  height: 50%;
  background-color: darkgoldenrod;
}
#image_view{
  height: 50%;
  background-color: greenyellow;
}

表示結果はこちら。

f:id:mslGt:20190419230542j:plain

何となくで書いた割には良い感じですね( ´∀`)bグッ!

中のアイテムを追加していく

では中身を追加していきます。

まずは header から。

タイトルとボタンを追加してみます。

mainPage.html

~省略~
< header id="global_header">
    < h1>Hello world!< /h1>
    < button id='header_share'>Share< /button>
< /header>
~省略~

結果はこちら。

f:id:mslGt:20190419230638j:plain

。。。謎のスペースが生まれています。

この理由は先ほど追加した h1 。

margin-top がデフォルトで追加されているらしく、その分が白いスペースとして表示されていたようです。

ということで、 h1 の margin-top を 0 にします。

ついでに margin-left 、 テキストカラーも変えておきます。

page.css

~省略~
header h1{
  color: whitesmoke;
  margin-top: 0;
  margin-left: 10px;
}
~省略~

両端に並べたい

今のままだと文字とボタンが左寄せで表示されているので、これを両端に並べてみます。

page.css

~省略~
header{
  width: 100%;
  height: 5%;
  background-color: cornflowerblue;
  display: flex;
  flex-direction: row;
  justify-content: space-between;
}
~省略~

mainPage.html

~省略~
< header id="global_header">
    < h1>Hello world!< /h1>
    < nav id='header_share_area'>
        < button class='header_share'>Share1< /button>
        < button class='header_share'>Share2< /button>
    < /nav>
< /header>
~省略~

justify-content: space-between を使うことで、子どもの要素を両端 → 中間の順で並べてくれます。

f:id:mslGt:20190419230751j:plain

左側のレイアウト

左の領域は検索条件を指定する領域にしたいと思います。

ということで、思い付きで要素を並べます。

mainPage.html

~省略~
< section id='search_area'>
    < div id='search_frame'>
        < div id='search_title'>検索< /div>
        < div id='search_query'>
            < label id='search_title_query'>キーワードで検索< /label>
            < input type='text' id='search_query_text'>< /input>
        < /div>
        < div id='search_check'>                        
            < label>チェック< /label>
            < ul>
                < li>
                    < input type='checkbox' value='search_availale'>available< /input>
                < /li>
                < li>
                    < input type='checkbox' value='search_availale_2'>available2< /input>
                < /li>
            < /ul>
        < /div>
        < div id='release_data'>
            < label>発売日< /label>
            < input type='date' value='search_relase_date_from'>< /input>
            < label>~< /label>
            < input type='date' value='search_relase_date_to'>< /input>
        < /div>
        < div id='search_submit'>
            < button id='search_button'>検索< /button>
        < /div>
    < /div>
< /section>
~省略~

そのまま表示するとこんな感じ。

f:id:mslGt:20190419230913j:plain

これを縦に並べていきたいと思います。

テキストボックス

とりあえず簡単そうかつ上の要素であるテキストやテキストボックスから。

page.css

~省略~
#search_frame{
  width: 90%;
  height: 90%;
  margin: 5%;
  background-color: white;
}
#search_title{
  text-align: center;
  color: aliceblue;
  background-color: green;
  height: 4%;
  margin-top: 2%;
}
#search_query{
  height: 10%;
  padding-top: 5%;
  width: 80%;
  margin-left: 10%;
}
#search_query input{
  width: 100%;
}
~省略~

input (テキストボックス) の幅を 100% にしているのは、他の要素の幅と合わせたかったためです。

チェックボックス

次はチェックボックス

横に並んでいるため、まず縦並びにする必要があります。

最初は下記のように flexbox を使って並べようと考えていました。

mainPage.html

~省略~
< div id='search_check'>                        
    < label>チェック< /label>
    < input type='checkbox' value='search_availale'>available< /input>
    < input type='checkbox' value='search_availale_2'>available2< /input>
< /div>
~省略~

page.css

~省略~
#search_check{
  height: 14%;
  width: 80%;
  margin-left: 10%;

  display: flex;
  flex-direction: column;
}
~省略~

結果はこの通り。

f:id:mslGt:20190419231003j:plain

チェックボックスの □ とラベル部分が別々に並べられてしまいました/(^o^)\

flexbox を使って正しく並べる方法もあるだろうとは思うのですが、今回はリストを使って並べてみることにしました。

mainPage.html

~省略~
< div id='search_check'>                        
    < label>チェック< /label>
    < ul>
        < li>
            < input type='checkbox' value='search_availale'>available< /input>
        < /li>
        < li>
            < input type='checkbox' value='search_availale_2'>available2< /input>
        < /li>
    < /ul>
< /div>
~省略~

page.css

~省略~
#search_check{
  height: 14%;
  width: 80%;
  margin-left: 10%;
}
#search_check ul{
  list-style: none;
  padding-top: 0;
  margin-top: 0;
  padding-left: 0;
}
#search_check li{
  margin-top: 1%;
}
~省略~

ul において、 list-style を none にすることで・をなくし、デフォルトでは左のラインより右に表示されてしまうので、 padding-left を 0 にしています。

残り(テキストボックス、ボタン)

残りの日付入力用のテキストボックスと、検索ボタンも配置します。

mainPage.html

~省略~
< div id='release_data'>
    < label>発売日< /label>
    < input type='date' value='search_relase_date_from'>< /input>
    < label>~< /label>
    < input type='date' value='search_relase_date_to'>< /input>
< /div>
< div id='search_submit'>
    < button id='search_button'>検索< /button>
< /div>
~省略~

page.css

~省略~
#release_data{
  display: flex;
  flex-direction: column;
  height: 18%;
  width: 80%;
  margin-left: 10%;
}
#release_data input{
  width: 100%;
}
#search_submit{
  width: 80%;
  margin-left: auto;
  margin-right: 10%;
  text-align: right;
}
#search_submit button{
  width: 80px;
}
~省略~

ボタンがなかなか右寄せできずにあれこれ試していたのですが、 text-align で一発という。。。

ラベルはともかく、 text-align が何に有効なのかを把握しておかないと地味にハマりそうです。

結果

結果としてこうなりました。

f:id:mslGt:20190419231105j:plain

もう少しデザイン的にこだわった方がとかある気はしますが、いったんこれで止めて次に進めることにします。
(白い枠の部分はもう少し縦を短くしても良さそうですが)

ほとんど触れませんでしたが、 input の type を date にするだけで、 DatePicker まで表示してくれるのはすごいですね!

f:id:mslGt:20190419231147j:plain

ただ、これブラウザごとに違いがあったりするようです
(下記は 2017 年の情報であり、 Firefox を含めた今のモダンブラウザは違う状況だとは思いますが)。

。。。ん? ブラウザごとの違い。。。?