All Downloads are FREE. Search and download functionalities are using the official Maven repository.

step.handlers.javahandler.jsonschema.KeywordJsonSchemaCreator Maven / Gradle / Ivy

The newest version!
/*******************************************************************************
 * Copyright (C) 2020, exense GmbH
 *
 * This file is part of STEP
 *
 * STEP is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * STEP 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 Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with STEP.  If not, see .
 ******************************************************************************/
package step.handlers.javahandler.jsonschema;

import jakarta.json.JsonArrayBuilder;
import jakarta.json.JsonException;
import jakarta.json.JsonObject;
import jakarta.json.JsonObjectBuilder;
import jakarta.json.spi.JsonProvider;
import jakarta.json.stream.JsonParsingException;
import step.handlers.javahandler.Input;
import step.handlers.javahandler.Keyword;

import java.io.StringReader;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.lang.reflect.Type;
import java.util.*;

import static step.handlers.javahandler.JsonInputConverter.resolveGenericTypeForArrayAndCollection;

public class KeywordJsonSchemaCreator {

	private final JsonProvider jsonProvider = JsonProvider.provider();

	private final JsonSchemaCreator jsonSchemaCreator = new JsonSchemaCreator(
			jsonProvider,
			(objectClass, field, fieldMetadata, propertiesBuilder, requiredPropertiesOutput, schemaCreator) -> false,
			new KeywordInputMetadataExtractor()
	);

	/**
	 * Creates a json schema for java method annotated with {@link Keyword} annotation
	 */
	public JsonObject createJsonSchemaForKeyword(Method method) throws JsonSchemaPreparationException{
		Keyword keywordAnnotation = method.getAnnotation(Keyword.class);
		if (keywordAnnotation == null) {
			throw new JsonSchemaPreparationException("Method is not annotated with Keyword annotation");
		}
		String functionName = keywordAnnotation.name().length() > 0 ? keywordAnnotation.name() : method.getName();

		// use explicit (plain text) schema specified in annotation
		boolean useTextJsonSchema = keywordAnnotation.schema() != null && !keywordAnnotation.schema().isEmpty();

		// build json schema via @Input annotations taken from method parameters
		boolean useAnnotatedJsonInputs = method.getParameters() != null && Arrays.stream(method.getParameters()).anyMatch(p -> p.isAnnotationPresent(Input.class));
		if (useTextJsonSchema && useAnnotatedJsonInputs) {
			throw new IllegalArgumentException("Ambiguous definition of json schema for keyword '" + functionName + "'. You should use either '@Input' annotation or define the 'schema' element of the @Keyword annotation");
		}

		if (useTextJsonSchema) {
			return readJsonSchemaFromPlainText(keywordAnnotation.schema(), method);
		} else if (useAnnotatedJsonInputs) {
			return readJsonSchemaFromInputAnnotations(method);
		} else {
			return createEmptyJsonSchema();
		}
	}

	private JsonObject readJsonSchemaFromInputAnnotations(Method method) throws JsonSchemaPreparationException {
		Keyword keywordAnnotation = method.getAnnotation(Keyword.class);
		String functionName = keywordAnnotation.name().length() > 0 ? keywordAnnotation.name() : method.getName();
		JsonObjectBuilder topLevelBuilder = jsonProvider.createObjectBuilder();
		// top-level type is always 'object'
		topLevelBuilder.add("type", "object");

		JsonObjectBuilder propertiesBuilder = jsonProvider.createObjectBuilder();
		List requiredProperties = new ArrayList<>();
		for (Parameter p : method.getParameters()) {
			JsonObjectBuilder propertyParamsBuilder = jsonProvider.createObjectBuilder();

			if (!p.isAnnotationPresent(Input.class)) {
				throw new JsonSchemaPreparationException("Parameter " + p.getName() + " is not annotated with " + Input.class.getName() + " for keyword " + functionName);
			}

			Input inputAnnotation = p.getAnnotation(Input.class);

			String parameterName = inputAnnotation.name();
			if(parameterName == null || parameterName.isEmpty()){
				throw new JsonSchemaPreparationException("The mandatory 'name' element of the Input annotation is missing for parameter " + p.getName() + " in keyword " + functionName);
			}

			Class type = p.getType();
			String propertyType = JsonInputConverter.resolveJsonPropertyType(type);
			propertyParamsBuilder.add("type", propertyType);

			if (propertyType.equals("array")) {
				//add items type
				Type parameterizedType = p.getParameterizedType();
				Class aClass = resolveGenericTypeForArrayAndCollection(JsonInputConverter.resolveClass(parameterizedType, parameterName), parameterizedType, parameterName);
				String arrayElementType = JsonInputConverter.resolveJsonPropertyType(aClass);
				JsonObject arrayType = jsonProvider.createObjectBuilder().add("type", arrayElementType).build();
				propertyParamsBuilder.add("items", arrayType);
			}

			if (inputAnnotation.defaultValue() != null && !inputAnnotation.defaultValue().isEmpty()) {
				try {
					JsonSchemaCreator.addDefaultValue(inputAnnotation.defaultValue(), propertyParamsBuilder, p.getParameterizedType(), parameterName);
				} catch (JsonSchemaPreparationException e) {
					throw new JsonSchemaPreparationException("Schema creation error for keyword '"
							+ functionName + "': " + e.getMessage());
				}
			}

			if (inputAnnotation.required()) {
				requiredProperties.add(parameterName);
			}

			if(Objects.equals("object", propertyType)) {
				//do not process nested fields for Maps, but add an empty property object
				if (Map.class.isAssignableFrom(type)) {
					propertyParamsBuilder.add("properties", jsonProvider.createObjectBuilder());
				} else {
					try {
						jsonSchemaCreator.processNestedFields(propertyParamsBuilder, p.getType());
					} catch (JsonSchemaPreparationException e) {
						throw new JsonSchemaPreparationException("Schema creation error for keyword '"
								+ functionName + "': " + e.getMessage());
					}
				}
			}

			propertiesBuilder.add(parameterName, propertyParamsBuilder);
		}
		topLevelBuilder.add("properties", propertiesBuilder);

		JsonArrayBuilder requiredBuilder = jsonProvider.createArrayBuilder();
		for (String requiredProperty : requiredProperties) {
			requiredBuilder.add(requiredProperty);
		}
		topLevelBuilder.add("required", requiredBuilder);
		return topLevelBuilder.build();
	}

	protected JsonObject createEmptyJsonSchema() {
		return jsonProvider.createObjectBuilder().build();
	}

	protected JsonObject readJsonSchemaFromPlainText(String schema, Method method) throws JsonSchemaPreparationException {
		try {
			return jsonProvider.createReader(new StringReader(schema)).readObject();
		} catch (JsonParsingException e) {
			throw new JsonSchemaPreparationException("Parsing error in the schema for keyword '" + method.getName() + "'. The error was: " + e.getMessage());
		} catch (JsonException e) {
			throw new JsonSchemaPreparationException("I/O error in the schema for keyword '" + method.getName() + "'. The error was: " + e.getMessage());
		} catch (Exception e) {
			throw new JsonSchemaPreparationException("Unknown error in the schema for keyword '" + method.getName() + "'. The error was: " + e.getMessage());
		}
	}

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy