-
Notifications
You must be signed in to change notification settings - Fork 34
Issue 11201 #11202
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: persisted-schema-poc
Are you sure you want to change the base?
Issue 11201 #11202
Changes from all commits
738c47f
369a9b6
a2077a4
13e851c
1da054f
e15a1ba
e18e334
b83c433
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| package com.enonic.xp.lib.schema; | ||
|
|
||
| import java.io.IOException; | ||
| import java.io.UncheckedIOException; | ||
| import java.util.Set; | ||
|
|
||
| import com.fasterxml.jackson.databind.ObjectMapper; | ||
| import com.fasterxml.jackson.databind.node.ArrayNode; | ||
| import com.fasterxml.jackson.databind.node.ObjectNode; | ||
|
|
||
| final class FormItemsJsonSchemaGenerator | ||
| { | ||
| private static final ObjectMapper MAPPER = new ObjectMapper(); | ||
|
|
||
| private final Set<String> inputTypeSchemaIds; | ||
|
|
||
| FormItemsJsonSchemaGenerator( final Set<String> inputTypeSchemaIds ) | ||
| { | ||
| this.inputTypeSchemaIds = inputTypeSchemaIds; | ||
| } | ||
|
|
||
| public String generate() | ||
| { | ||
| final String schemaId = "https://xp.enonic.com/schemas/json/form-items.schema.json"; | ||
|
|
||
| final ObjectNode schema = MAPPER.createObjectNode(); | ||
|
|
||
| schema.put( "$schema", "https://json-schema.org/draft/2020-12/schema" ); | ||
| schema.put( "$id", schemaId ); | ||
|
|
||
| final ArrayNode oneOf = MAPPER.createArrayNode(); | ||
|
|
||
| oneOf.add( ref( "https://xp.enonic.com/schemas/json/field-set.schema.json" ) ); | ||
| oneOf.add( ref( "https://xp.enonic.com/schemas/json/item-set.schema.json" ) ); | ||
| oneOf.add( ref( "https://xp.enonic.com/schemas/json/inline-mixin.schema.json" ) ); | ||
| oneOf.add( ref( "https://xp.enonic.com/schemas/json/option-set.schema.json" ) ); | ||
|
|
||
| inputTypeSchemaIds.forEach( id -> oneOf.add( ref( id ) ) ); | ||
|
|
||
| schema.set( "oneOf", oneOf ); | ||
|
|
||
| try | ||
| { | ||
| return MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString( schema ); | ||
| } | ||
| catch ( IOException e ) | ||
| { | ||
| throw new UncheckedIOException( e ); | ||
| } | ||
| } | ||
|
|
||
| private ObjectNode ref( final String refPath ) | ||
| { | ||
| final ObjectNode ref = MAPPER.createObjectNode(); | ||
| ref.put( "$ref", refPath ); | ||
| return ref; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| package com.enonic.xp.lib.schema; | ||
|
|
||
| public record JsonSchemaDefinitionWrapper(String schema, boolean inputType) | ||
| { | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,98 @@ | ||
| package com.enonic.xp.lib.schema; | ||
|
|
||
| import java.io.IOException; | ||
| import java.io.UncheckedIOException; | ||
| import java.nio.file.DirectoryStream; | ||
| import java.nio.file.Files; | ||
| import java.nio.file.Path; | ||
| import java.nio.file.Paths; | ||
| import java.util.Map; | ||
| import java.util.Objects; | ||
| import java.util.Set; | ||
| import java.util.concurrent.ConcurrentHashMap; | ||
| import java.util.concurrent.ConcurrentMap; | ||
| import java.util.stream.Collectors; | ||
|
|
||
| import com.fasterxml.jackson.databind.JsonNode; | ||
| import com.fasterxml.jackson.databind.ObjectMapper; | ||
|
|
||
| public final class JsonSchemaRegistry | ||
| { | ||
| private static final ObjectMapper MAPPER = new ObjectMapper(); | ||
|
|
||
| private final ConcurrentMap<String, JsonSchemaDefinitionWrapper> schemasMap = new ConcurrentHashMap<>(); | ||
|
|
||
| public JsonSchemaRegistry() | ||
| { | ||
|
|
||
| } | ||
|
|
||
| public void activate() | ||
| { | ||
| registerBuiltinSchemas(); | ||
| } | ||
|
|
||
| public String register( final String jsonSchemaDef ) | ||
| { | ||
| return register( jsonSchemaDef, false ); | ||
| } | ||
|
|
||
| public String registerInputType( final String jsonSchemaDef ) | ||
| { | ||
| return register( jsonSchemaDef, true ); | ||
| } | ||
|
|
||
| private String register( final String jsonSchemaDef, final boolean inputType ) | ||
| { | ||
| try | ||
| { | ||
| final JsonNode schemaNode = MAPPER.readTree( jsonSchemaDef ); | ||
| final String schemaId = Objects.requireNonNull( schemaNode.get( "$id" ), "$id must be set" ).asText(); | ||
| schemasMap.put( schemaId, new JsonSchemaDefinitionWrapper( jsonSchemaDef, inputType ) ); | ||
| return schemaId; | ||
| } | ||
| catch ( IOException e ) | ||
| { | ||
| throw new UncheckedIOException( e ); | ||
| } | ||
| } | ||
|
|
||
| public Set<String> getInputTypeSchemaIds() | ||
| { | ||
| return schemasMap.entrySet() | ||
| .stream() | ||
| .filter( entry -> entry.getValue().inputType() ) | ||
| .map( Map.Entry::getKey ) | ||
| .collect( Collectors.toUnmodifiableSet() ); | ||
| } | ||
|
|
||
| public Map<String, String> getAllSchemas() | ||
| { | ||
| return schemasMap.entrySet() | ||
| .stream() | ||
| .collect( Collectors.toUnmodifiableMap( Map.Entry::getKey, entry -> entry.getValue().schema() ) ); | ||
| } | ||
|
|
||
| private void registerBuiltinSchemas() | ||
| { | ||
| schemasMap.clear(); | ||
| registerBuiltinSchemasFromDir( Paths.get( "src/main/resources/schemas/inputTypes" ), true ); | ||
| registerBuiltinSchemasFromDir( Paths.get( "src/main/resources/schemas" ), false ); | ||
| } | ||
|
|
||
| private void registerBuiltinSchemasFromDir( final Path dirPath, final boolean inputType ) | ||
| { | ||
| try (DirectoryStream<Path> stream = Files.newDirectoryStream( dirPath, "*.json" )) | ||
| { | ||
| for ( Path path : stream ) | ||
| { | ||
| final String schemaDef = Files.readString( path ); | ||
| register( schemaDef, inputType ); | ||
| } | ||
| } | ||
| catch ( IOException e ) | ||
| { | ||
| throw new UncheckedIOException( e ); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,10 @@ | ||||||
| package com.enonic.xp.lib.schema; | ||||||
|
|
||||||
| public interface JsonSchemaService | ||||||
| { | ||||||
| String registerInputTypeSchema( final String jsonSchemaDefinition ); | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ℹ️ Codacy found a minor CodeStyle issue: Redundant 'final' modifier. The issue identified by the Checkstyle linter indicates that the To fix the issue, we can simply remove the Here’s the code suggestion for the change:
Suggested change
This comment was generated by an experimental AI tool. |
||||||
|
|
||||||
| boolean isContentTypeValid( final String contentTypeAsYml ); | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ℹ️ Codacy found a minor CodeStyle issue: Redundant 'final' modifier. The issue reported by the Checkstyle linter indicates that the Here's the suggested code change:
Suggested change
This comment was generated by an experimental AI tool. |
||||||
|
|
||||||
| boolean isSchemaValid( final String schemaId, final String yml ); | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ℹ️ Codacy found a minor CodeStyle issue: Redundant 'final' modifier. The issue reported by the Checkstyle linter indicates that the To fix the issue, we can simply remove the
Suggested change
This comment was generated by an experimental AI tool. |
||||||
| } | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| package com.enonic.xp.lib.schema; | ||
|
|
||
| import java.util.Set; | ||
|
|
||
| import com.networknt.schema.InputFormat; | ||
| import com.networknt.schema.JsonSchema; | ||
| import com.networknt.schema.JsonSchemaFactory; | ||
| import com.networknt.schema.SchemaLocation; | ||
| import com.networknt.schema.SpecVersion; | ||
| import com.networknt.schema.ValidationMessage; | ||
|
|
||
| public class JsonSchemaServiceImpl | ||
| implements JsonSchemaService | ||
| { | ||
| private final JsonSchemaRegistry jsonSchemaRegistry; | ||
|
|
||
| private JsonSchemaFactory schemaFactory; | ||
|
|
||
| public JsonSchemaServiceImpl( final JsonSchemaRegistry jsonSchemaRegistry ) | ||
| { | ||
| this.jsonSchemaRegistry = jsonSchemaRegistry; | ||
| } | ||
|
|
||
| public void activate() | ||
| { | ||
| final FormItemsJsonSchemaGenerator formItemsJsonSchemaGenerator = | ||
| new FormItemsJsonSchemaGenerator( jsonSchemaRegistry.getInputTypeSchemaIds() ); | ||
|
|
||
| final String formItemsSchema = formItemsJsonSchemaGenerator.generate(); | ||
| jsonSchemaRegistry.register( formItemsSchema ); | ||
|
|
||
| this.schemaFactory = JsonSchemaFactory.getInstance( SpecVersion.VersionFlag.V202012, builder -> builder.schemaLoaders( | ||
| loader -> loader.schemas( jsonSchemaRegistry.getAllSchemas() ) ) ); | ||
| } | ||
|
|
||
| @Override | ||
| public String registerInputTypeSchema( final String jsonSchemaDefinition ) | ||
| { | ||
| return jsonSchemaRegistry.registerInputType( jsonSchemaDefinition ); | ||
| } | ||
|
|
||
| @Override | ||
| public boolean isContentTypeValid( final String contentTypeAsYml ) | ||
| { | ||
| return isSchemaValid( "https://xp.enonic.com/schemas/json/content-type.schema.json", contentTypeAsYml ); | ||
| } | ||
|
|
||
| @Override | ||
| public boolean isSchemaValid( final String schemaId, final String yml ) | ||
| { | ||
| final JsonSchema schema = schemaFactory.getSchema( SchemaLocation.of( schemaId ) ); | ||
| schema.initializeValidators(); | ||
|
|
||
| Set<ValidationMessage> errors = | ||
| schema.validate( yml, InputFormat.YAML, ctx -> ctx.getExecutionConfig().setFormatAssertionsEnabled( true ) ); | ||
|
|
||
| final boolean valid = errors.isEmpty(); | ||
|
|
||
| if ( !valid ) | ||
| { | ||
| System.out.println( "Validation errors:" ); | ||
| errors.forEach( err -> System.out.println( "- " + err.getMessage() ) ); | ||
| } | ||
|
|
||
| return valid; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,111 @@ | ||
| package com.enonic.xp.lib.schema; | ||
|
|
||
| import java.util.List; | ||
| import java.util.Map; | ||
| import java.util.Objects; | ||
| import java.util.stream.Collectors; | ||
|
|
||
| import com.enonic.xp.app.ApplicationKey; | ||
| import com.enonic.xp.app.ApplicationRelativeResolver; | ||
| import com.enonic.xp.schema.content.ContentType; | ||
| import com.enonic.xp.schema.xdata.XDataName; | ||
| import com.enonic.xp.schema.xdata.XDataNames; | ||
|
|
||
| public final class YmlContentTypeParser | ||
| { | ||
| private ContentType.Builder builder; | ||
|
|
||
| private ApplicationKey currentApplication; | ||
|
|
||
| private ApplicationRelativeResolver resolver; | ||
|
|
||
| public YmlContentTypeParser builder( final ContentType.Builder builder ) | ||
| { | ||
| this.builder = builder; | ||
| return this; | ||
| } | ||
|
|
||
| public YmlContentTypeParser currentApplication( final ApplicationKey currentApplication ) | ||
| { | ||
| this.currentApplication = currentApplication; | ||
| return this; | ||
| } | ||
|
|
||
| @SuppressWarnings("unchecked") | ||
| public void parse( final Map<String, Object> contentTypeAsMap ) | ||
| { | ||
| this.resolver = new ApplicationRelativeResolver( this.currentApplication ); | ||
|
|
||
| builder.name( "article" ); | ||
|
|
||
| parseDisplayName( contentTypeAsMap ); | ||
| parseDescription( contentTypeAsMap ); | ||
|
|
||
| builder.superType( resolver.toContentTypeName( getTrimmedValue( (String) contentTypeAsMap.get( "superType" ) ) ) ); | ||
| builder.setAbstract( Objects.requireNonNullElse( (Boolean) contentTypeAsMap.get( "isAbstract" ), false ) ); | ||
| builder.setFinal( Objects.requireNonNullElse( (Boolean) contentTypeAsMap.get( "isFinal" ), false ) ); | ||
| builder.allowChildContent( Objects.requireNonNullElse( (Boolean) contentTypeAsMap.get( "allowChildContent" ), true ) ); | ||
|
|
||
| final List<String> allowChildContentTypes = (List<String>) contentTypeAsMap.get( "allowChildContentType" ); | ||
| if ( allowChildContentTypes != null ) | ||
| { | ||
| builder.allowChildContentType( allowChildContentTypes.stream() | ||
| .filter( Objects::nonNull ) | ||
| .map( String::trim ) | ||
| .filter( s -> !s.isEmpty() ) | ||
| .collect( Collectors.toList() ) ); | ||
| } | ||
|
|
||
| final List<Map<String, Object>> xDataElements = (List<Map<String, Object>>) contentTypeAsMap.get( "xData" ); | ||
| if ( xDataElements != null ) | ||
| { | ||
| final List<XDataName> names = xDataElements.stream().map( xDataElement -> { | ||
| final String xDataName = (String) xDataElement.get( "name" ); | ||
| return resolver.toXDataName( xDataName ); | ||
| } ).toList(); | ||
| builder.xData( XDataNames.from( names ) ); | ||
| } | ||
|
|
||
| final YmlFormParser formParser = new YmlFormParser( currentApplication ); | ||
| builder.form( formParser.parse( (List<Map<String, Object>>) contentTypeAsMap.get( "form" ) ) ); | ||
| } | ||
|
|
||
| @SuppressWarnings("unchecked") | ||
| private void parseDisplayName( final Map<String, Object> contentTypeAsMap ) | ||
| { | ||
| final Map<String, Object> displayName = (Map<String, Object>) contentTypeAsMap.get( "displayName" ); | ||
|
|
||
| if ( displayName != null ) | ||
| { | ||
| builder.displayName( getTrimmedValue( (String) displayName.get( "text" ) ) ); | ||
| builder.displayNameI18nKey( (String) displayName.get( "i18n" ) ); | ||
| builder.displayNameExpression( getTrimmedValue( (String) displayName.get( "expression" ) ) ); | ||
| } | ||
|
|
||
| final Map<String, Object> displayNameLabel = (Map<String, Object>) contentTypeAsMap.get( "label" ); | ||
|
|
||
| if ( displayNameLabel != null ) | ||
| { | ||
| builder.displayNameLabel( getTrimmedValue( (String) displayNameLabel.get( "text" ) ) ); | ||
| builder.displayNameLabelI18nKey( (String) displayNameLabel.get( "i18n" ) ); | ||
| } | ||
| } | ||
|
|
||
| @SuppressWarnings("unchecked") | ||
| private void parseDescription( final Map<String, Object> contentTypeAsMap ) | ||
| { | ||
| final Map<String, Object> description = (Map<String, Object>) contentTypeAsMap.get( "description" ); | ||
|
|
||
| if ( description != null ) | ||
| { | ||
| builder.description( getTrimmedValue( (String) description.get( "text" ) ) ); | ||
| builder.descriptionI18nKey( (String) description.get( "i18n" ) ); | ||
| } | ||
| } | ||
|
|
||
| private String getTrimmedValue( final String value ) | ||
| { | ||
| return value != null ? value.trim() : null; | ||
| } | ||
|
|
||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ℹ️ Codacy found a minor CodeStyle issue: Avoid unnecessary constructors - the compiler will generate these for you
The issue identified by the PMD linter is that the constructor
public JsonSchemaRegistry()is unnecessary because the Java compiler automatically generates a default constructor if no other constructors are defined in the class. Since there are no additional initializations or parameters needed in the constructor, it can be removed to adhere to cleaner code practices.To fix the issue, you can simply remove the explicit constructor. Here’s the code suggestion:
This change effectively eliminates the unnecessary constructor, allowing the compiler to provide the default constructor implicitly.
This comment was generated by an experimental AI tool.