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}