001package com.streamconverter.util;
002
003import java.io.InputStream;
004import java.net.URL;
005import java.util.Objects;
006
007/**
008 * クラスパスリソースの取得を行うユーティリティクラス
009 *
010 * <p>JAR内にバンドルされたリソースを安全に取得します。
011 *
012 * <p>セキュリティ:
013 *
014 * <ul>
015 *   <li>ClassLoaderはクラスパス内でパス正規化を行う("hoge/../fuga" → "fuga")
016 *   <li>クラスパス境界外へのアクセスは不可能("../etc/passwd" → リソース未発見)
017 *   <li>JAR内リソースは読み取り専用(改ざん不可)
018 *   <li>シンボリックリンク攻撃は不可能(ファイルシステムではない)
019 * </ul>
020 *
021 * <p>用途:
022 *
023 * <ul>
024 *   <li>スキーマファイル(XSD, JSON Schema等)
025 *   <li>テンプレートファイル
026 *   <li>設定ファイル(application.properties等)
027 * </ul>
028 */
029public final class ClasspathResourceValidator {
030
031  private ClasspathResourceValidator() {
032    throw new UnsupportedOperationException("Utility class");
033  }
034
035  /**
036   * クラスパスリソースを取得します
037   *
038   * <p>ClassLoaderを使用してリソースを取得します。クラスパス境界外へのアクセスは不可能です。
039   *
040   * <p>注意事項:
041   *
042   * <ul>
043   *   <li>リソース名はスラッシュ(/)区切りで指定(例: "schemas/test.xsd")
044   *   <li>先頭にスラッシュを含めない(ClassLoaderの仕様)
045   *   <li>パス正規化あり: "hoge/../fuga" → "fuga" として解決される
046   *   <li>クラスパス外アクセス不可: "../etc/passwd" → リソース未発見
047   * </ul>
048   *
049   * @param resourcePath クラスパスからの相対パス(例: "schemas/test.xsd")
050   * @return リソースのInputStream
051   * @throws NullPointerException パスがnullの場合
052   * @throws IllegalArgumentException パスが空、またはリソースが存在しない場合
053   */
054  public static InputStream getResourceAsStream(String resourcePath) {
055    Objects.requireNonNull(resourcePath, "Resource path cannot be null");
056
057    // 先頭スラッシュを除去(ClassLoaderの仕様に合わせる)
058    String normalizedPath = resourcePath.startsWith("/") ? resourcePath.substring(1) : resourcePath;
059
060    if (normalizedPath.isEmpty()) {
061      throw new IllegalArgumentException("Resource path cannot be empty");
062    }
063
064    // Context ClassLoaderを優先し、なければクラスのClassLoaderを使用
065    ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
066    if (classLoader == null) {
067      classLoader = ClasspathResourceValidator.class.getClassLoader();
068    }
069
070    InputStream stream = classLoader.getResourceAsStream(normalizedPath);
071
072    if (stream == null) {
073      throw new IllegalArgumentException("Resource not found: " + normalizedPath);
074    }
075
076    return stream;
077  }
078
079  /**
080   * クラスパスリソースのURLを取得します
081   *
082   * <p>ClassLoaderを使用してリソースを取得します。クラスパス境界外へのアクセスは不可能です。
083   *
084   * <p>注意事項:
085   *
086   * <ul>
087   *   <li>リソース名はスラッシュ(/)区切りで指定(例: "schemas/test.xsd")
088   *   <li>先頭にスラッシュを含めない(ClassLoaderの仕様)
089   *   <li>パス正規化あり: "hoge/../fuga" → "fuga" として解決される
090   *   <li>クラスパス外アクセス不可: "../etc/passwd" → リソース未発見
091   * </ul>
092   *
093   * @param resourcePath クラスパスからの相対パス
094   * @return リソースのURL
095   * @throws NullPointerException パスがnullの場合
096   * @throws IllegalArgumentException パスが空、またはリソースが存在しない場合
097   */
098  public static URL getResourceUrl(String resourcePath) {
099    Objects.requireNonNull(resourcePath, "Resource path cannot be null");
100
101    // 先頭スラッシュを除去(ClassLoaderの仕様に合わせる)
102    String normalizedPath = resourcePath.startsWith("/") ? resourcePath.substring(1) : resourcePath;
103
104    if (normalizedPath.isEmpty()) {
105      throw new IllegalArgumentException("Resource path cannot be empty");
106    }
107
108    // Context ClassLoaderを優先し、なければクラスのClassLoaderを使用
109    ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
110    if (classLoader == null) {
111      classLoader = ClasspathResourceValidator.class.getClassLoader();
112    }
113
114    URL url = classLoader.getResource(normalizedPath);
115
116    if (url == null) {
117      throw new IllegalArgumentException("Resource not found: " + normalizedPath);
118    }
119
120    return url;
121  }
122}