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  private ValidateCommand(String schemaPath, Schema schema) {
049    this.schemaPath = schemaPath;
050    this.schema = schema;
051  }
052
053  /**
054   * Factory method for creating a ValidateCommand.
055   *
056   * @param schemaPath クラスパスリソース識別子(例: "schemas/test.xsd", "test-schema.xsd")
057   * @return a ValidateCommand instance
058   * @throws NullPointerException スキーマパスがnullの場合
059   * @throws IllegalArgumentException スキーマパスが空の場合
060   * @throws StreamProcessingException スキーマファイルの読み込みに失敗した場合
061   */
062  public static ValidateCommand create(String schemaPath) {
063    Objects.requireNonNull(schemaPath, "Schema path cannot be null");
064    if (schemaPath.trim().isEmpty()) {
065      throw new IllegalArgumentException("Schema path cannot be empty");
066    }
067    String normalizedPath = normalizeClasspathPath(schemaPath);
068    Schema schema = loadSchemaFromClasspath(normalizedPath);
069    return new ValidateCommand(normalizedPath, schema);
070  }
071
072  /**
073   * クラスパスリソース識別子を正規化します
074   *
075   * <p>先頭のスラッシュはClassLoader互換性のため除去されます。
076   *
077   * @param inputPath 入力されたクラスパス識別子
078   * @return 正規化されたクラスパス識別子
079   */
080  private static String normalizeClasspathPath(String inputPath) {
081    String trimmed = inputPath.trim();
082    // Remove leading slash for ClassLoader compatibility
083    if (trimmed.startsWith("/")) {
084      trimmed = trimmed.substring(1);
085    }
086    return trimmed;
087  }
088
089  /**
090   * セキュアにスキーマをロードします
091   *
092   * @param validatedPath 検証済みのスキーマパス
093   * @return ロードされたSchemaオブジェクト
094   * @throws StreamProcessingException スキーマロードに失敗した場合
095   */
096  private static Schema loadSchemaFromClasspath(String validatedPath) {
097    try {
098      // セキュアなSchemaFactoryの作成(新しいセキュリティインフラを使用)
099      SchemaFactory factory = SecureXmlConfiguration.createSecureSchemaFactory();
100
101      // クラスパスからスキーマをロード(パストラバーサル不要・JAR対応)
102      URL schemaUrl = ClasspathResourceValidator.getResourceUrl(validatedPath);
103
104      Schema loadedSchema = factory.newSchema(schemaUrl);
105      logger.info("XML Schema loaded successfully from: {}", validatedPath);
106      securityLogger.info("Secure XML schema loading completed for: {}", validatedPath);
107
108      return loadedSchema;
109
110    } catch (SAXException | IllegalArgumentException e) {
111      logger.error("Failed to load XML schema from {}: {}", validatedPath, e.getMessage(), e);
112      securityLogger.error("Secure XML schema loading failed for: {}", validatedPath);
113      throw new StreamProcessingException(
114          String.format("XMLスキーマの読み込みに失敗しました - スキーマ: %s, エラー: %s", validatedPath, e.getMessage()),
115          e);
116    }
117  }
118
119  /**
120   * XMLのバリデーションを行うコマンドを実行します。
121   *
122   * <p>XMLのスキーマを指定して、XMLのバリデーションを行います。
123   *
124   * <p>バリデーションエラーが発生した場合は、エラーメッセージを出力します。
125   *
126   * @param inputStream 入力ストリーム
127   * @throws IOException 入出力エラーが発生した場合
128   * @throws StreamProcessingException XXE防止設定の適用失敗またはXMLバリデーションエラーが発生した場合
129   */
130  @Override
131  public void consume(InputStream inputStream) throws IOException {
132    Objects.requireNonNull(inputStream, "Input stream cannot be null");
133
134    try {
135      // セキュアなValidatorの作成
136      Validator validator = schema.newValidator();
137      configureSecureValidator(validator);
138
139      // XMLバリデーションの実行
140      validator.validate(new StreamSource(inputStream));
141
142      logger.info("XML validation completed successfully using schema: {}", schemaPath);
143
144    } catch (SAXException e) {
145      // バリデーションエラーの詳細ログ出力
146      logger.error("XMLバリデーションエラーが発生しました: {}", e.getMessage(), e);
147
148      // バリデーションエラーをカスタム例外でラップして伝播
149      throw new StreamProcessingException(
150          String.format("XMLバリデーションに失敗しました - スキーマ: %s, エラー: %s", schemaPath, e.getMessage()), e);
151    }
152  }
153
154  /**
155   * Validatorにセキュリティ設定を適用します
156   *
157   * @param validator 設定対象のValidator
158   * @throws SAXException セキュリティ設定に失敗した場合(XXE脆弱性のまま継続しないためスロー)
159   */
160  private void configureSecureValidator(Validator validator) throws SAXException {
161    // XXE攻撃防止設定(設定失敗はXXE脆弱性のまま継続するため例外をスロー)
162    validator.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
163
164    // 外部リソースアクセスを無効化
165    validator.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, "");
166    validator.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");
167
168    logger.debug("Secure XML processing features configured for Validator");
169  }
170}