001package com.streamconverter.command.impl.json;
002
003import com.fasterxml.jackson.core.JsonParser;
004import com.fasterxml.jackson.databind.JsonNode;
005import com.fasterxml.jackson.databind.ObjectMapper;
006import com.networknt.schema.JsonSchema;
007import com.networknt.schema.JsonSchemaFactory;
008import com.networknt.schema.SpecVersion;
009import com.networknt.schema.ValidationMessage;
010import com.streamconverter.StreamProcessingException;
011import com.streamconverter.command.ConsumerCommand;
012import java.io.File;
013import java.io.IOException;
014import java.io.InputStream;
015import java.util.Objects;
016import java.util.Set;
017import org.slf4j.Logger;
018import org.slf4j.LoggerFactory;
019
020/**
021 * JSONスキーマバリデーションを行うコマンドクラス
022 *
023 * <p>JSONスキーマファイルを使用してJSONデータのバリデーションを実行します。 バリデーションエラーが発生した場合は、詳細なエラー情報とともに例外をスローします。
024 *
025 * <p><strong>技術的制約について:</strong><br>
026 * JSON Schema検証では構造全体の検証が必要なため、完全なストリーミング処理は技術的に困難です。 本実装では任意サイズのデータを受け入れつつ、Jackson streaming
027 * APIを使用してメモリ効率を最大化しています。
028 *
029 * <p><strong>完全ストリーミング処理について:</strong><br>
030 * 真のストリーミング処理が必要な場合は{@link JsonStreamingValidateCommand}の使用を検討してください。
031 * JsonSurferによる完全ストリーミング検証で、任意サイズのデータを一定メモリで処理できます。
032 *
033 * <p>使用例:
034 *
035 * <pre>
036 * JsonValidateCommand validator = new JsonValidateCommand("schema/user.json");
037 * validator.consume(jsonInputStream);
038 * </pre>
039 */
040public class JsonValidateCommand extends ConsumerCommand {
041  private static final Logger logger = LoggerFactory.getLogger(JsonValidateCommand.class);
042
043  private final String schemaPath;
044  private final ObjectMapper objectMapper;
045  private final JsonSchemaFactory schemaFactory;
046
047  /**
048   * コンストラクタ
049   *
050   * @param schemaPath JSONスキーマファイルのパス
051   * @throws IllegalArgumentException スキーマパスがnullまたは空の場合
052   * @throws StreamProcessingException スキーマファイルの読み込みに失敗した場合
053   */
054  public JsonValidateCommand(String schemaPath) {
055    this.schemaPath = validateSchemaPath(schemaPath);
056    this.objectMapper = new ObjectMapper();
057    this.schemaFactory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V7);
058
059    // コンストラクタでスキーマファイルの妥当性を検証
060    try {
061      loadSchema();
062    } catch (StreamProcessingException e) {
063      throw e;
064    }
065  }
066
067  /** スキーマパスの検証 */
068  private String validateSchemaPath(String path) {
069    if (path == null) {
070      throw new IllegalArgumentException("Schema path cannot be null");
071    }
072    String trimmedPath = path.trim();
073    if (trimmedPath.isEmpty()) {
074      throw new IllegalArgumentException("Schema path cannot be empty");
075    }
076    return trimmedPath;
077  }
078
079  /**
080   * JSONバリデーションを実行します
081   *
082   * @param inputStream 検証対象のJSONデータを含む入力ストリーム
083   * @throws IOException I/Oエラーが発生した場合
084   * @throws StreamProcessingException JSONバリデーションエラーが発生した場合
085   */
086  @Override
087  public void consume(InputStream inputStream) throws IOException {
088    Objects.requireNonNull(inputStream, "InputStream cannot be null");
089
090    logger.info("Starting JSON validation with schema: {}", schemaPath);
091
092    try {
093      // スキーマファイルの読み込み
094      JsonSchema schema = loadSchema();
095
096      // JSONデータのストリーミング解析 - 任意サイズのデータに対応
097      // 注意: JSON Schema検証では全体構造の検証が必要なため、完全なストリーミング処理は技術的に困難
098      // しかし、Jackson streaming APIを使用してメモリ効率を最大化
099      JsonNode jsonNode;
100      try {
101        try (JsonParser parser = objectMapper.createParser(inputStream)) {
102          // Jackson streaming APIを使用してJSONを解析
103          jsonNode = objectMapper.readTree(parser);
104          if (jsonNode == null) {
105            throw new StreamProcessingException(
106                "Failed to parse JSON input: Input stream is empty or contains no valid JSON data");
107          }
108
109          logger.debug("JSON data loaded successfully, validating against schema");
110        }
111      } catch (StreamProcessingException e) {
112        throw e;
113      } catch (Exception e) {
114        throw new StreamProcessingException("Failed to parse JSON input: " + e.getMessage(), e);
115      }
116
117      // バリデーション実行
118      Set<ValidationMessage> validationMessages = schema.validate(jsonNode);
119
120      if (validationMessages.isEmpty()) {
121        logger.info("JSON validation completed successfully");
122      } else {
123        handleValidationErrors(validationMessages);
124      }
125
126    } catch (StreamProcessingException e) {
127      // 既にラップされた例外はそのまま再スロー
128      throw e;
129    } catch (Exception e) {
130      logger.error("JSON validation failed: {}", e.getMessage(), e);
131      throw new StreamProcessingException(
132          String.format(
133              "JSON validation failed - schema: %s, error: %s", schemaPath, e.getMessage()),
134          e);
135    }
136  }
137
138  /** JSONスキーマを読み込み */
139  private JsonSchema loadSchema() throws StreamProcessingException {
140    try {
141      File schemaFile = new File(schemaPath);
142      if (!schemaFile.exists()) {
143        throw new StreamProcessingException(
144            "Failed to load JSON schema: Schema file not found: " + schemaPath);
145      }
146
147      if (!schemaFile.canRead()) {
148        throw new StreamProcessingException(
149            "Failed to load JSON schema: Schema file is not readable: " + schemaPath);
150      }
151
152      JsonNode schemaNode;
153      try {
154        schemaNode = objectMapper.readTree(schemaFile);
155      } catch (Exception e) {
156        throw new StreamProcessingException(
157            "Failed to load JSON schema: Invalid schema file format: " + schemaPath, e);
158      }
159
160      if (schemaNode == null) {
161        throw new StreamProcessingException(
162            "Failed to load JSON schema: Schema file is empty: " + schemaPath);
163      }
164
165      return schemaFactory.getSchema(schemaNode);
166
167    } catch (StreamProcessingException e) {
168      throw e;
169    } catch (Exception e) {
170      throw new StreamProcessingException("Failed to load JSON schema from: " + schemaPath, e);
171    }
172  }
173
174  /** バリデーションエラーの処理 */
175  private void handleValidationErrors(Set<ValidationMessage> validationMessages) {
176    StringBuilder errorBuilder = new StringBuilder();
177    errorBuilder
178        .append("JSON validation failed with ")
179        .append(validationMessages.size())
180        .append(" validation errors:");
181
182    int errorCount = 0;
183    for (ValidationMessage message : validationMessages) {
184      errorBuilder.append("\n  ").append(++errorCount).append(". ");
185      errorBuilder.append("Path: ").append(message.getInstanceLocation());
186      errorBuilder.append(" - ").append(message.getMessage());
187
188      // ログに詳細を出力
189      logger.error(
190          "JSON validation error - Path: {}, Message: {}",
191          message.getInstanceLocation(),
192          message.getMessage());
193    }
194
195    String errorMessage = errorBuilder.toString();
196    logger.error("JSON validation summary: {}", errorMessage);
197
198    throw new StreamProcessingException(
199        String.format("JSON validation failed - schema: %s, errors: %s", schemaPath, errorMessage));
200  }
201
202  /**
203   * スキーマパスを取得
204   *
205   * @return スキーマファイルのパス
206   */
207  public String getSchemaPath() {
208    return schemaPath;
209  }
210}