001package com.streamconverter.command.impl.xml;
002
003import com.streamconverter.StreamProcessingException;
004import com.streamconverter.command.ConsumerCommand;
005import com.streamconverter.security.SecureXmlConfiguration;
006import com.streamconverter.util.ClasspathResourceValidator;
007import java.io.IOException;
008import java.io.InputStream;
009import java.net.URL;
010import java.util.Objects;
011import javax.xml.XMLConstants;
012import javax.xml.transform.stream.StreamSource;
013import javax.xml.validation.Schema;
014import javax.xml.validation.SchemaFactory;
015import javax.xml.validation.Validator;
016import org.slf4j.Logger;
017import org.slf4j.LoggerFactory;
018import org.xml.sax.SAXException;
019
020/**
021 * XMLのバリデーションを行うコマンドクラス
022 *
023 * <p>XMLのバリデーションを行うコマンドクラスです。
024 *
025 * <p>このクラスは、XMLのスキーマを指定して、XMLのバリデーションを行います。
026 *
027 * <p>バリデーションエラーが発生した場合は、エラーメッセージを出力します。
028 */
029public class ValidateCommand extends ConsumerCommand {
030  private static final Logger logger = LoggerFactory.getLogger(ValidateCommand.class);
031  private static final Logger securityLogger =
032      LoggerFactory.getLogger("com.streamConverter.security");
033
034  private final String schemaPath;
035  private final Schema schema;
036
037  /**
038   * コンストラクタ
039   *
040   * <p>クラスパスからXMLスキーマを読み込み、バリデーションコマンドを作成します。
041   *
042   * <p>セキュリティ: ClassLoaderはクラスパス内でパス正規化を行います(例: "hoge/../fuga" → "fuga")。
043   * ただし、クラスパス境界外へのアクセスは不可能です("../etc/passwd" → リソース未発見)。
044   *
045   * @param schemaPath クラスパスリソース識別子(例: "schemas/test.xsd", "test-schema.xsd")
046   * @throws StreamProcessingException スキーマファイルの読み込みに失敗した場合
047   */
048  public ValidateCommand(String schemaPath) {
049    Objects.requireNonNull(schemaPath, "Schema path cannot be null");
050    if (schemaPath.trim().isEmpty()) {
051      throw new IllegalArgumentException("Schema path cannot be empty");
052    }
053
054    // クラスパスリソース識別子として扱う(セキュリティはClasspathResourceValidatorが担保)
055    this.schemaPath = normalizeClasspathPath(schemaPath);
056    this.schema = loadSchemaFromClasspath(this.schemaPath);
057  }
058
059  /**
060   * クラスパスリソース識別子を正規化します
061   *
062   * <p>先頭のスラッシュはClassLoader互換性のため除去されます。
063   *
064   * @param inputPath 入力されたクラスパス識別子
065   * @return 正規化されたクラスパス識別子
066   */
067  private String normalizeClasspathPath(String inputPath) {
068    String trimmed = inputPath.trim();
069    // Remove leading slash for ClassLoader compatibility
070    if (trimmed.startsWith("/")) {
071      trimmed = trimmed.substring(1);
072    }
073    return trimmed;
074  }
075
076  /**
077   * セキュアにスキーマをロードします
078   *
079   * @param validatedPath 検証済みのスキーマパス
080   * @return ロードされたSchemaオブジェクト
081   * @throws StreamProcessingException スキーマロードに失敗した場合
082   */
083  private Schema loadSchemaFromClasspath(String validatedPath) {
084    try {
085      // セキュアなSchemaFactoryの作成(新しいセキュリティインフラを使用)
086      SchemaFactory factory = SecureXmlConfiguration.createSecureSchemaFactory();
087
088      // クラスパスからスキーマをロード(パストラバーサル不要・JAR対応)
089      URL schemaUrl = ClasspathResourceValidator.getResourceUrl(validatedPath);
090
091      Schema loadedSchema = factory.newSchema(schemaUrl);
092      logger.info("XML Schema loaded successfully from: {}", validatedPath);
093      securityLogger.info("Secure XML schema loading completed for: {}", validatedPath);
094
095      return loadedSchema;
096
097    } catch (SAXException | IllegalArgumentException e) {
098      logger.error("Failed to load XML schema from {}: {}", validatedPath, e.getMessage(), e);
099      securityLogger.error("Secure XML schema loading failed for: {}", validatedPath);
100      throw new StreamProcessingException(
101          String.format("XMLスキーマの読み込みに失敗しました - スキーマ: %s, エラー: %s", validatedPath, e.getMessage()),
102          e);
103    }
104  }
105
106  /**
107   * XMLのバリデーションを行うコマンドを実行します。
108   *
109   * <p>XMLのスキーマを指定して、XMLのバリデーションを行います。
110   *
111   * <p>バリデーションエラーが発生した場合は、エラーメッセージを出力します。
112   *
113   * @param inputStream 入力ストリーム
114   * @throws IOException 入出力エラーが発生した場合
115   * @throws StreamProcessingException XMLバリデーションエラーが発生した場合
116   */
117  @Override
118  public void consume(InputStream inputStream) throws IOException {
119    Objects.requireNonNull(inputStream, "Input stream cannot be null");
120
121    try {
122      // セキュアなValidatorの作成
123      Validator validator = schema.newValidator();
124      configureSecureValidator(validator);
125
126      // XMLバリデーションの実行
127      validator.validate(new StreamSource(inputStream));
128
129      logger.info("XML validation completed successfully using schema: {}", schemaPath);
130
131    } catch (SAXException e) {
132      // バリデーションエラーの詳細ログ出力
133      logger.error("XMLバリデーションエラーが発生しました: {}", e.getMessage(), e);
134
135      // バリデーションエラーをカスタム例外でラップして伝播
136      throw new StreamProcessingException(
137          String.format("XMLバリデーションに失敗しました - スキーマ: %s, エラー: %s", schemaPath, e.getMessage()), e);
138    }
139  }
140
141  /**
142   * Validatorにセキュリティ設定を適用します
143   *
144   * @param validator 設定対象のValidator
145   */
146  private void configureSecureValidator(Validator validator) {
147    try {
148      // XXE攻撃防止設定
149      validator.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
150
151      // 外部リソースアクセスを無効化
152      validator.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, "");
153      validator.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");
154
155      logger.debug("Secure XML processing features configured for Validator");
156
157    } catch (Exception e) {
158      logger.warn("Could not configure all security features for Validator: {}", e.getMessage());
159      // 警告レベルで記録し、処理は継続
160    }
161  }
162}