NetworkNTJsonschemaValidator.java

/*
 * GovWay - A customizable API Gateway 
 * https://govway.org
 * 
 * Copyright (c) 2005-2024 Link.it srl (https://link.it). 
 * 
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 3, as published by
 * the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

package org.openspcoop2.utils.json.validation;

import java.io.ByteArrayOutputStream;
import java.util.Set;

import org.openspcoop2.utils.LoggerWrapperFactory;
import org.openspcoop2.utils.json.IJsonSchemaValidator;
import org.openspcoop2.utils.json.JSONUtils;
import org.openspcoop2.utils.json.JsonSchemaValidatorConfig;
import org.openspcoop2.utils.json.ValidationException;
import org.openspcoop2.utils.json.ValidationResponse;
import org.openspcoop2.utils.json.ValidationResponse.ESITO;
import org.slf4j.Logger;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.networknt.schema.JsonSchema;
import com.networknt.schema.JsonSchemaFactory;
import com.networknt.schema.SchemaValidatorsConfig;
import com.networknt.schema.SpecVersion;
import com.networknt.schema.SpecVersionDetector;
import com.networknt.schema.ValidationMessage;

/**
 * NetworkNTJsonschemaValidator
 *
 * @author Giovanni Bussu (bussu@link.it)
 * @author $Author$
 * @version $Rev$, $Date$
 */
public class NetworkNTJsonschemaValidator implements IJsonSchemaValidator {

	/*
	 * NOTA: in caso di errore 'should be valid to one and only one of the schemas'
	 * 
	 * Significa che più elementi possono matchare in un oneOf. Questo succede se ad esempio non è stato definito "additionalProperties: false" in ogni oggetto riferito dal oneOf
	 * 
	 * NOTA2: Aggiungendo le additionalProperties a false, se si aggiungono male e non si ha un match con nessun oggetto, viene ritornato l'errore relativo ad uno dei tre a caso.
	 * */
	
	private JsonSchema schema;
	private byte[] schemaBytes;
	private JsonNode jsonSchema;
	
	private ObjectMapper mapper;
	private Logger log;
	private boolean logError;

	// Tramite un cast e' possibile cosi personalizzare lo schema validator con le opzioni di NT
	private SchemaValidatorsConfig schemaValidatorConfig = null;
	public SchemaValidatorsConfig getSchemaValidatorConfig() {
		return this.schemaValidatorConfig;
	}
	public void setSchemaValidatorConfig(SchemaValidatorsConfig schemaValidatorConfig) {
		this.schemaValidatorConfig = schemaValidatorConfig;
	}
	
	/**
	 * 
	 */
	public NetworkNTJsonschemaValidator() {	
		this.mapper = new ObjectMapper();
	}
	@Override
	public void setSchema(byte[] schema, JsonSchemaValidatorConfig config, Logger log) throws ValidationException {
		
		this.log = log;
		if(this.log==null) {
			this.log = LoggerWrapperFactory.getLogger(NetworkNTJsonschemaValidator.class);
		}
		this.logError = config!=null ? config.isEmitLogError() : true;
		this.schemaBytes = schema;
		
		try {
			JsonNode jsonSchema =  this.mapper.readTree(schema);
	        
			SpecVersion.VersionFlag version	= null;
			if(config!=null && config.getJsonSchemaVersion()!=null) {
				version = config.getJsonSchemaVersion();
			}
			else {
				version = SpecVersionDetector.detect(jsonSchema);
			}
			
	        JsonSchemaFactory factory = JsonSchemaFactory.getInstance(version);
			
	        if(config!=null) {
				switch(config.getAdditionalProperties()) {
				case DEFAULT:
					break;
				case FORCE_DISABLE: ValidationUtils.disableAdditionalProperties(this.mapper, jsonSchema, true, true);
					break;
				case FORCE_STRING: ValidationUtils.disableAdditionalProperties(this.mapper, jsonSchema, false, true);
					break;
				case IF_NULL_DISABLE: ValidationUtils.disableAdditionalProperties(this.mapper, jsonSchema, true, false);
					break;
				case IF_NULL_STRING: ValidationUtils.disableAdditionalProperties(this.mapper, jsonSchema, false, false);
					break;
				default:
					break;
				}
	        }
	        
	        if(config!=null) {
				switch(config.getPoliticaInclusioneTipi()) {
				case DEFAULT:
					break;
				case ALL: ValidationUtils.addTypes(this.mapper, jsonSchema, config.getTipi(), true);
					break;
				case ANY: ValidationUtils.addTypes(this.mapper, jsonSchema, config.getTipi(), false);
					break;
				default:
					break;
				}
	        }

			if(config!=null && config.isVerbose()) {
				try {
					ByteArrayOutputStream bout = new ByteArrayOutputStream();
					JSONUtils.getInstance(true).writeTo(jsonSchema, bout);
					bout.flush();
					bout.close();
					this.log.debug("JSON Schema: "+bout.toString());
				}catch(Exception e) {
					this.log.debug("JSON Schema build error: "+e.getMessage(),e);
				}
			}
			
			this.jsonSchema = jsonSchema;
			/*
			 * https://github.com/networknt/json-schema-validator/blob/master/doc/config.md
			 * 
			 * SchemaValidatorsConfig configS = new SchemaValidatorsConfig();
			 * configS.setHandleNullableField(false);
			 * configS.setTypeLoose(true);
			 **/
			if(this.schemaValidatorConfig!=null) {
				this.schema = factory.getSchema(jsonSchema, this.schemaValidatorConfig);
			}
			else {
				this.schema = factory.getSchema(jsonSchema);
			}
	        
		} catch(Throwable e) {
			throw new ValidationException(e);
		}
	}
	



	@Override
	public ValidationResponse validate(byte[] rawObject) throws ValidationException {
		
		ValidationResponse response = new ValidationResponse();
		try {
			boolean expectedString = false;
			if(this.jsonSchema.has("type")) {
				try {
					JsonNode type = this.jsonSchema.get("type");
					String vType = type.asText();
					expectedString = "string".equals(vType);
				}catch(Exception e) {}
			}
			
	        JsonNode node = null;
	        try {
	        	if(expectedString) {
	        		node = this.mapper.getNodeFactory().textNode(new String(rawObject));
	        	}
	        	else {
	        		node = this.mapper.readTree(rawObject);
	        	}
	        }
	        catch(Exception e) {
	        	this.log.error(e.getMessage(),e);
	        	String messageString = "Read rawObject as jsonNode failed: "+e.getMessage();
	        	response.setEsito(ESITO.KO);
	        	if(this.logError) {
	        		ValidationUtils.logError(this.log, messageString.toString(), rawObject, this.schemaBytes, this.jsonSchema);
	        	}
				response.setException(new Exception(messageString.toString()));
	        }

	        if(node!=null) {
				Set<ValidationMessage> validate = this.schema.validate(node);
				if(validate.isEmpty()) {
					response.setEsito(ESITO.OK);	
				} else {
					response.setEsito(ESITO.KO);
					StringBuilder messageString = new StringBuilder();
					for(ValidationMessage msg: validate) {
						String errorMsg = msg.getCode() + " " + msg.getMessage();
						response.getErrors().add(errorMsg);
						messageString.append(errorMsg).append("\n");
					}
					if(this.logError) {
		        		ValidationUtils.logError(this.log, messageString.toString(), rawObject, this.schemaBytes, this.jsonSchema);
					}
					response.setException(new Exception(messageString.toString()));
	
				}
	        }
			
		} catch(Exception e) {
			throw new ValidationException(e);
		}
		return response;
	}
	
}