AbstractApiValidator.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.rest;

import java.net.URL;
import java.util.ArrayList;
import java.util.List;

import org.openspcoop2.utils.UtilsMultiException;
import org.openspcoop2.utils.rest.api.Api;
import org.openspcoop2.utils.rest.api.ApiBodyParameter;
import org.openspcoop2.utils.rest.api.ApiCookieParameter;
import org.openspcoop2.utils.rest.api.ApiHeaderParameter;
import org.openspcoop2.utils.rest.api.ApiOperation;
import org.openspcoop2.utils.rest.api.ApiParameterSchema;
import org.openspcoop2.utils.rest.api.ApiParameterSchemaComplexType;
import org.openspcoop2.utils.rest.api.ApiParameterTypeSchema;
import org.openspcoop2.utils.rest.api.ApiRequestDynamicPathParameter;
import org.openspcoop2.utils.rest.api.ApiRequestFormParameter;
import org.openspcoop2.utils.rest.api.ApiRequestQueryParameter;
import org.openspcoop2.utils.rest.api.ApiResponse;
import org.openspcoop2.utils.rest.api.ApiSchemaTypeRestriction;
import org.openspcoop2.utils.rest.api.ApiUtilities;
import org.openspcoop2.utils.rest.entity.Cookie;
import org.openspcoop2.utils.rest.entity.HttpBaseEntity;
import org.openspcoop2.utils.rest.entity.HttpBaseRequestEntity;
import org.openspcoop2.utils.rest.entity.HttpBaseResponseEntity;
import org.openspcoop2.utils.transport.TransportUtils;
import org.openspcoop2.utils.transport.http.ContentTypeUtilities;
import org.springframework.web.util.UriUtils;

/**
 * ApiValidatorConfig
 *
 *
 * @author Poli Andrea (apoli@link.it)
 * @author $Author$
 * @version $Rev$, $Date$
 */
public abstract class AbstractApiValidator   {

	public abstract void validatePreConformanceCheck(HttpBaseEntity<?> httpEntity,ApiOperation operation,Object ... args) throws ProcessingException,ValidatorException;
	
	public abstract void validatePostConformanceCheck(HttpBaseEntity<?> httpEntity,ApiOperation operation,Object ... args) throws ProcessingException,ValidatorException;
	
	public abstract void validateValueAsType(ApiParameterType parameterType, String value, String type, ApiSchemaTypeRestriction typeRestriction) throws ProcessingException,ValidatorException;
	
	public void validate(Api api, HttpBaseEntity<?> httpEntity, Object ... args) throws ProcessingException,ValidatorException{
		
		ApiOperation operation = api.findOperation(httpEntity.getMethod(), httpEntity.getUrl());
		if(operation==null){
			throw new ProcessingException("Resource "+httpEntity.getMethod()+" '"+httpEntity.getUrl()+"' not found");
		}

		// es. xsd
		validatePreConformanceCheck(httpEntity, operation, args);
		
		validateConformanceCheck(httpEntity, operation, api.getBaseURL());
		
		// es. elementi specifici come nomi nel xsd etc..
		validatePostConformanceCheck(httpEntity, operation, args);
		
	}
	
	private void validateConformanceCheck(HttpBaseEntity<?> httpEntity,ApiOperation operation, URL baseUrl) throws ProcessingException,ValidatorException{

		try{
			
			
			
			if(httpEntity.getContentType() != null) {
				
				String baseTypeHttp = ContentTypeUtilities.readBaseTypeFromContentType(httpEntity.getContentType());
				
				boolean contentTypeSupported = false;
				List<ApiBodyParameter> requestBodyParametersList = null;
				List<ApiResponse> responses = null;
				if(operation.getRequest()!=null &&  operation.getRequest().sizeBodyParameters()>0){
					requestBodyParametersList = operation.getRequest().getBodyParameters();
				}
				if(operation.sizeResponses()>0){
					responses = operation.getResponses();
				}
				int status = -1;
				
				if(httpEntity instanceof HttpBaseRequestEntity<?>) {
										
					if(requestBodyParametersList != null) {
						for(ApiBodyParameter input: requestBodyParametersList) {
							if(input.isAllMediaType() || ContentTypeUtilities.isMatch(baseTypeHttp, input.getMediaType())) {
								contentTypeSupported = true;
								break;
							} 
						}
					}
				} else if(httpEntity instanceof HttpBaseResponseEntity<?>) {
					status = ((HttpBaseResponseEntity<?>)httpEntity).getStatus();
					
					if(responses != null) {
						
						// Fix: se si traccia del codice http esatto non devo andare a verificare il codice di default
						
						// prima faccio la verifica con codice esatto
						ApiResponse outputDefault = null;
						boolean findExactResponseCode = false;
						for(ApiResponse output: responses) {
							if(status==output.getHttpReturnCode()) {
								findExactResponseCode = true;
								if(output.sizeBodyParameters()>0) {
									for(ApiBodyParameter outputBodyParameter: output.getBodyParameters()) {
										if(outputBodyParameter.isAllMediaType() || ContentTypeUtilities.isMatch(baseTypeHttp, outputBodyParameter.getMediaType()) ) {
											contentTypeSupported = true;
											break;
										} 
									}
								}
							}
							else if(output.isDefaultHttpReturnCode()) {
								outputDefault = output;
							}
						}
						
						// poi con l'eventuale default
						if(!contentTypeSupported && !findExactResponseCode && outputDefault!=null) {
							if(outputDefault.sizeBodyParameters()>0) {
								for(ApiBodyParameter outputBodyParameter: outputDefault.getBodyParameters()) {
									if(outputBodyParameter.isAllMediaType() || ContentTypeUtilities.isMatch(baseTypeHttp, outputBodyParameter.getMediaType()) ) {
										contentTypeSupported = true;
										break;
									} 
								}
							}
						}
					}
	
				}
				
				if(!contentTypeSupported) {
					if(status>0)
						throw new ValidatorException("Content-Type '"+baseTypeHttp+"' (http response status '"+status+"') unsupported");
					else
						throw new ValidatorException("Content-Type '"+baseTypeHttp+"' unsupported");
				}
			}
			else {
				
				// senza content-type
				
				if(httpEntity instanceof HttpBaseRequestEntity<?>) {
					
					if(operation.getRequest()!=null &&  operation.getRequest().sizeBodyParameters()>0){
						
						boolean required = false;
						for(ApiBodyParameter input: operation.getRequest().getBodyParameters()) {
							if(input.isRequired()) {
								required = true;
							}
						}
						if(required) {
							throw new ValidatorException("Request without payload (Content-Type 'null') unsupported");
						}
						
					}
					
				}
				
				
			}
			
			if(httpEntity instanceof HttpBaseRequestEntity<?>) {
			
				HttpBaseRequestEntity<?> request = (HttpBaseRequestEntity<?>) httpEntity;
				
				if(operation.getRequest()!=null &&  operation.getRequest().sizeHeaderParameters()>0){
					for (ApiHeaderParameter paramHeader : operation.getRequest().getHeaderParameters()) {
						String name = paramHeader.getName();
						List<String> values = TransportUtils.getRawObject(request.getHeaders(), name);
						if(values==null || values.isEmpty()){
							if(paramHeader.isRequired()){
								throw new ValidatorException("Required http header '"+name+"' not found");
							}
						}
						else {
							for (String value : values) {
								if(value!=null){
									validate(paramHeader.getApiParameterSchema(), ApiParameterType.header, name, value, "http header");
								}	
							}
						}
					}
				}
				
				if(operation.getRequest()!=null &&  operation.getRequest().sizeCookieParameters()>0){
					for (ApiCookieParameter paramCookie : operation.getRequest().getCookieParameters()) {
						String name = paramCookie.getName();
						String value = null;
						if(request.getCookies()!=null){
							for (Cookie cookie : request.getCookies()) {
								if(name.equalsIgnoreCase(cookie.getName())){
									value = cookie.getValue();
								}
							}
						}
						if(value==null){
							if(paramCookie.isRequired()){
								throw new ValidatorException("Required Cookie '"+name+"' not found");
							}
						}
						if(value!=null){
							validate(paramCookie.getApiParameterSchema(), ApiParameterType.cookie, name, value, "cookie");
						}
					}
				}
				
				if(operation.getRequest()!=null &&  operation.getRequest().sizeQueryParameters()>0){
					for (ApiRequestQueryParameter paramQuery : operation.getRequest().getQueryParameters()) {
						
						// i parametri sono esplosi. La validazione viene fatta con json o openapi	
						boolean parametriEsplosi = false;
						if(paramQuery.getApiParameterSchema()!=null && paramQuery.getApiParameterSchema().getSchemas()!=null && !paramQuery.getApiParameterSchema().getSchemas().isEmpty()) {
							for (ApiParameterTypeSchema schema : paramQuery.getApiParameterSchema().getSchemas()) {
								// Il controllo basta farlo sul primo.
								if(schema.getSchema()!=null && schema.getSchema().isTypeObject()) {
									if (
											(schema.getSchema().isStyleQueryForm() && schema.getSchema().isExplodeEnabled())
											||
											(schema.getSchema().isStyleQueryDeepObject())
											) {
										parametriEsplosi = true;
										break;
									}
								}
							}
						}
						if(parametriEsplosi) {
							continue;
						}
						
						String name = paramQuery.getName();
						List<String> values = TransportUtils.getRawObject(request.getParameters(), name);
						if(values==null || values.isEmpty()){
							if(paramQuery.isRequired()){
								throw new ValidatorException("Required query parameter '"+name+"' not found");
							}
						}
						else {
							for (String value : values) {
								if(value!=null){
									validate(paramQuery.getApiParameterSchema(), ApiParameterType.query, name, value, "query parameter");
								}
							}
						}
					}
				}
								
				if(operation.getRequest()!=null &&  operation.getRequest().sizeDynamicPathParameters()>0){
					for (ApiRequestDynamicPathParameter paramDynamicPath : operation.getRequest().getDynamicPathParameters()) {
						boolean find = false;
						String valueFound = null;
						if(operation.isDynamicPath()){
							for (int i = 0; i < operation.sizePath(); i++) {
								if(operation.isDynamicPath()){
									String idDinamic = operation.getDynamicPathId(i);
									if(paramDynamicPath.getName().equals(idDinamic)){
										String [] urlList = ApiUtilities.extractUrlList(baseUrl, request.getUrl());
										if(i>=urlList.length){
											throw new ValidatorException("Dynamic path '"+paramDynamicPath.getName()+"' not found (position:"+i+", urlLenght:"+urlList.length+")");
										}
										find = true;
										valueFound = urlList[i];
										break;
									}
								}
							}
						}
						if(!find && paramDynamicPath.isRequired()){
							throw new ValidatorException("Required dynamic path '"+paramDynamicPath.getName()+"' not found");
						}
						if(find){
							String valueUrlDecoded = valueFound;
							// il valore può essere url encoded
							try {
								// Note that Java’s URLEncoder class encodes space character(" ") into a + sign. 
								// This is contrary to other languages like Javascript that encode space character into %20.
								/*
								 *  URLEncoder is not for encoding URLs, but for encoding parameter names and values for use in GET-style URLs or POST forms. 
								 *  That is, for transforming plain text into the application/x-www-form-urlencoded MIME format as described in the HTML specification. 
								 **/
								//valueUrlDecoded = java.net.URLDecoder.decode(valueFound, org.openspcoop2.utils.resources.Charset.UTF_8.getValue());
								
								valueUrlDecoded = UriUtils.decode(valueFound, org.openspcoop2.utils.resources.Charset.UTF_8.getValue());
								
								//System.out.println("DOPO '"+valueUrlDecoded+"' PRIMA '"+valueFound+"'");
								
							}catch(Throwable e) {
								//System.out.println("ERRORE");
								//e.printStackTrace(System.out);
								// utilizzo valore originale
								//throw new RuntimeException(e.getMessage(),e);
							}
							validate(paramDynamicPath.getApiParameterSchema(), ApiParameterType.path, paramDynamicPath.getName(), valueUrlDecoded, "dynamic path",
									(valueUrlDecoded!=null && !valueUrlDecoded.equals(valueFound)) ? valueFound : null);
						}
					}
				}
				
				if(operation.getRequest()!=null &&  operation.getRequest().sizeFormParameters()>0){
					for (ApiRequestFormParameter paramForm : operation.getRequest().getFormParameters()) {
						String name = paramForm.getName();
						List<String> values = TransportUtils.getRawObject(request.getParameters(), name);
						if(values==null || values.isEmpty()){
							if(paramForm.isRequired()){
								throw new ValidatorException("Required form parameter '"+name+"' not found");
							}
						}
						else {
							for (String value : values) {
								if(value!=null){
									validate(paramForm.getApiParameterSchema(), ApiParameterType.form, name, value, "form parameter");
								}
							}
						}
					}
				}
				
			}
			
			if(httpEntity instanceof HttpBaseResponseEntity<?>) {
				
				HttpBaseResponseEntity<?> response = (HttpBaseResponseEntity<?>) httpEntity;
				ApiResponse apiResponseFound = null;
				ApiResponse apiResponseDefault = null;
				
				for (ApiResponse apiResponse : operation.getResponses()) {
					if(apiResponse.isDefaultHttpReturnCode()) {
						apiResponseDefault = apiResponse;
					}
					if(response.getStatus() == apiResponse.getHttpReturnCode()){
						apiResponseFound = apiResponse;
						break;
					}										
				}
				
				if(apiResponseFound==null && apiResponseDefault!=null) {
					apiResponseFound = apiResponseDefault;
				}
				if(apiResponseFound==null){
					throw new ValidatorException("Http status code '"+response.getStatus()+"' unsupported");
				}
				
				if(apiResponseFound.sizeHeaderParameters()>0){
					for (ApiHeaderParameter paramHeader : apiResponseFound.getHeaderParameters()) {
						String name = paramHeader.getName();
						List<String> values = TransportUtils.getRawObject(response.getHeaders(), name);
						if(values==null || values.isEmpty()){
							if(paramHeader.isRequired()){
								throw new ValidatorException("Required http header '"+name+"' not found");
							}
						}
						else {
							for (String value : values) {
								if(value!=null){
									validate(paramHeader.getApiParameterSchema(), ApiParameterType.header, name, value, "http header");
								}
							}
						}
					}
				}
				
				if(apiResponseFound.sizeCookieParameters()>0){
					for (ApiCookieParameter paramCookie : apiResponseFound.getCookieParameters()) {
						String name = paramCookie.getName();
						String value = null;
						if(response.getCookies()!=null){
							for (Cookie cookie : response.getCookies()) {
								if(name.equalsIgnoreCase(cookie.getName())){
									value = cookie.getValue();
								}
							}
						}
						if(value==null){
							if(paramCookie.isRequired()){
								throw new ValidatorException("Required cookie '"+name+"' not found");
							}
						}
						if(value!=null){
							validate(paramCookie.getApiParameterSchema(), ApiParameterType.cookie, name, value, "cookie");
						}
					}
				}
				
			}

		}catch(ProcessingException e){
			throw e;
		}catch(ValidatorException e){
			throw e;
		}catch(Exception e){
			throw new ProcessingException(e.getMessage(),e);
		}
		
	}
	
	
	private void validate(ApiParameterSchema apiParameterSchema, ApiParameterType type, String name, String value, String position) throws ValidatorException, ProcessingException {
		this.validate(apiParameterSchema, type, name, value, position, null);
	}
	private void validate(ApiParameterSchema apiParameterSchema, ApiParameterType type, String name, String value, String position, String valueUrlEncoded) throws ValidatorException, ProcessingException {
		if(apiParameterSchema!=null && apiParameterSchema.getSchemas()!=null && !apiParameterSchema.getSchemas().isEmpty()) {
			ApiParameterSchemaComplexType ct = apiParameterSchema.getComplexType();
			if(ct==null) {
				ct = ApiParameterSchemaComplexType.simple;
			}
			
			String prefixError = "Invalid value '"+value+"' in "+position+" '"+name+"'";
			if(valueUrlEncoded!=null) {
				 prefixError = "Invalid value '"+value+"' (urlEncoded: '"+valueUrlEncoded+"') in "+position+" '"+name+"'";
			}
			
			switch (ct) {
			case simple: // caso speciale con 1 solo
			case allOf:
				StringBuilder sbException = new StringBuilder();
				List<ValidatorException> listValidatorException = new ArrayList<ValidatorException>();
				for (ApiParameterTypeSchema schema : apiParameterSchema.getSchemas()) {
					try{
						validateValueAsType(type, value, schema.getType(),schema.getSchema());
					}catch(ValidatorException val){
						String msgError = prefixError+" (expected type '"+schema.getType()+"'): "+val.getMessage();
						if(sbException.length()>0) {
							sbException.append("\n");
						}
						sbException.append(msgError);
						listValidatorException.add( new ValidatorException(msgError,val) );
					}	
				}
				if(!listValidatorException.isEmpty()) {
					UtilsMultiException multi = new UtilsMultiException(listValidatorException.toArray(new ValidatorException[1]));
					throw new ValidatorException(sbException.toString(), multi);
				}
				return;
				
			case anyOf:
				sbException = new StringBuilder();
				listValidatorException = new ArrayList<ValidatorException>();
				for (ApiParameterTypeSchema schema : apiParameterSchema.getSchemas()) {
					try{
						validateValueAsType(type, value, schema.getType(),schema.getSchema());
						return; // ne basta una che valida correttamente
					}catch(ValidatorException val){
						String msgError = prefixError+" (expected type '"+schema.getType()+"'): "+val.getMessage();
						if(sbException.length()>0) {
							sbException.append("\n");
						}
						sbException.append(msgError);
						listValidatorException.add( new ValidatorException(msgError,val) );
					}	
				}
				if(!listValidatorException.isEmpty()) {
					UtilsMultiException multi = new UtilsMultiException(listValidatorException.toArray(new ValidatorException[1]));
					throw new ValidatorException(sbException.toString(), multi);
				}
				return; //caso che non può accadere
				
			case oneOf:
				sbException = new StringBuilder();
				listValidatorException = new ArrayList<ValidatorException>();
				int schemiValidi = 0;
				for (ApiParameterTypeSchema schema : apiParameterSchema.getSchemas()) {
					try{
						validateValueAsType(type, value, schema.getType(),schema.getSchema());
						schemiValidi++;
					}catch(ValidatorException val){
						String msgError = prefixError+" (expected type '"+schema.getType()+"'): "+val.getMessage();
						if(sbException.length()>0) {
							sbException.append("\n");
						}
						sbException.append(msgError);
						listValidatorException.add( new ValidatorException(msgError,val) );
					}	
				}
				if(schemiValidi==0) {
					if(!listValidatorException.isEmpty()) {
						UtilsMultiException multi = new UtilsMultiException(listValidatorException.toArray(new ValidatorException[1]));
						throw new ValidatorException(sbException.toString(), multi);
					}
					else {
						throw new ValidatorException(prefixError);
					}
				}
				else if(schemiValidi>1) {
					throw new ValidatorException(prefixError+": expected validates the value against exactly one of the subschemas; founded valid in "+schemiValidi+" schemas");
				}
				return; // deve essere validato rispetto esattamente ad uno schema

			}
		}

		

	}
}