/* * SPDX-License-Identifier: Apache-2.0 * * The OpenSearch Contributors require contributions made to * this file be licensed under the Apache-2.0 license or a * compatible open source license. */ /* * Licensed to Elasticsearch under one or more contributor * license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright * ownership. Elasticsearch licenses this file to you under * the Apache License, Version 2.0 (the "License"); you may * not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ /* * Modifications Copyright OpenSearch Contributors. See * GitHub history for details. */ package org.opensearch.gradle.precommit; import com.fasterxml.jackson.databind.ObjectMapper; import com.networknt.schema.JsonSchema; import com.networknt.schema.JsonSchemaException; import com.networknt.schema.JsonSchemaFactory; import com.networknt.schema.SchemaValidatorsConfig; import com.networknt.schema.SpecVersion; import com.networknt.schema.ValidationMessage; import org.gradle.api.DefaultTask; import org.gradle.api.UncheckedIOException; import org.gradle.api.file.FileCollection; import org.gradle.api.tasks.InputFile; import org.gradle.api.tasks.InputFiles; import org.gradle.api.tasks.OutputFile; import org.gradle.api.tasks.TaskAction; import org.gradle.work.ChangeType; import org.gradle.work.Incremental; import org.gradle.work.InputChanges; import java.io.File; import java.io.IOException; import java.io.PrintWriter; import java.util.Collection; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.Map; import java.util.Set; import java.util.stream.StreamSupport; /** * Incremental task to validate a set of JSON files against against a schema. */ public class ValidateJsonAgainstSchemaTask extends DefaultTask { private final ObjectMapper mapper = new ObjectMapper(); private File jsonSchema; private File report; private FileCollection inputFiles; @Incremental @InputFiles public FileCollection getInputFiles() { return inputFiles; } public void setInputFiles(FileCollection inputFiles) { this.inputFiles = inputFiles; } @InputFile public File getJsonSchema() { return jsonSchema; } public void setJsonSchema(File jsonSchema) { this.jsonSchema = jsonSchema; } public void setReport(File report) { this.report = report; } @OutputFile public File getReport() { return this.report; } @TaskAction public void validate(InputChanges inputChanges) throws IOException { File jsonSchemaOnDisk = getJsonSchema(); getLogger().debug("JSON schema : [{}]", jsonSchemaOnDisk.getAbsolutePath()); SchemaValidatorsConfig config = new SchemaValidatorsConfig(); config.setEcma262Validator(true); JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V7); JsonSchema jsonSchema = factory.getSchema(mapper.readTree(jsonSchemaOnDisk), config); Map> errors = new LinkedHashMap<>(); // incrementally evaluate input files StreamSupport.stream(inputChanges.getFileChanges(getInputFiles()).spliterator(), false) .filter(f -> f.getChangeType() != ChangeType.REMOVED) .forEach(fileChange -> { File file = fileChange.getFile(); if (file.isDirectory() == false) { // validate all files and hold on to errors for a complete report if there are failures getLogger().debug("Validating JSON [{}]", file.getName()); try { Set validationMessages = jsonSchema.validate(mapper.readTree(file)); maybeLogAndCollectError(validationMessages, errors, file); } catch (IOException e) { throw new UncheckedIOException(e); } } }); if (errors.isEmpty()) { try (PrintWriter printWriter = new PrintWriter(getReport())) { printWriter.println("Success! No validation errors found."); } } else { try (PrintWriter printWriter = new PrintWriter(getReport())) { printWriter.printf("Schema: %s%n", jsonSchemaOnDisk); printWriter.println("----------Validation Errors-----------"); errors.values().stream().flatMap(Collection::stream).forEach(printWriter::println); } StringBuilder sb = new StringBuilder(); sb.append("Error validating JSON. See the report at: "); sb.append(getReport().toURI().toASCIIString()); sb.append(System.lineSeparator()); sb.append( String.format("JSON validation failed: %d files contained %d violations", errors.keySet().size(), errors.values().size()) ); throw new JsonSchemaException(sb.toString()); } } private void maybeLogAndCollectError(Set messages, Map> errors, File file) { for (ValidationMessage message : messages) { getLogger().error("[validate JSON][ERROR][{}][{}]", file.getName(), message.toString()); errors.computeIfAbsent(file, k -> new LinkedHashSet<>()) .add(String.format("%s: %s", file.getAbsolutePath(), message.toString())); } } }