Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions .idea/codeStyles/Project.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -148,4 +148,6 @@ prometheus-metrics-exposition-textformats = { module = "io.prometheus:prometheus
prometheus-metrics-model = { module = "io.prometheus:prometheus-metrics-model", version.ref = "prometheus" }
prometheus-metrics-tracer-common = { module = "io.prometheus:prometheus-metrics-tracer-common", version.ref = "prometheus" }

networknt = { module = "com.networknt:json-schema-validator", version = "1.5.8" }

[plugins]
5 changes: 5 additions & 0 deletions modules/lib/lib-schema/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,10 @@ dependencies {
compileOnly project( ':core:core-api' )
compileOnly project( ':script:script-api' )

implementation libs.jackson.dataformat.yaml
implementation libs.jackson.datatype.jsr310
implementation libs.jackson.core
implementation libs.networknt

testImplementation project( ':tools:testing' )
}
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()

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:

Suggested change
public JsonSchemaRegistry()
// public JsonSchemaRegistry() { }

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.

{

}

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 );

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: Redundant 'final' modifier.

The issue identified by the Checkstyle linter indicates that the final modifier is considered redundant in the method parameter declaration. In Java, method parameters are implicitly final, meaning they cannot be reassigned within the method body. Therefore, explicitly declaring them as final is unnecessary and can lead to code that is less clean and more difficult to read.

To fix the issue, we can simply remove the final keyword from the method parameter in the registerInputTypeSchema method.

Here’s the code suggestion for the change:

Suggested change
String registerInputTypeSchema( final String jsonSchemaDefinition );
String registerInputTypeSchema(String jsonSchemaDefinition);

This comment was generated by an experimental AI tool.


boolean isContentTypeValid( final String contentTypeAsYml );

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: Redundant 'final' modifier.

The issue reported by the Checkstyle linter indicates that the final modifier is considered redundant for the parameter contentTypeAsYml in the method isContentTypeValid. In Java, method parameters are implicitly final unless explicitly marked otherwise, meaning they cannot be reassigned within the method body. Thus, adding the final modifier is unnecessary and can be removed to comply with the coding standards.

Here's the suggested code change:

Suggested change
boolean isContentTypeValid( final String contentTypeAsYml );
boolean isContentTypeValid( String contentTypeAsYml );

This comment was generated by an experimental AI tool.


boolean isSchemaValid( final String schemaId, final String yml );

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: Redundant 'final' modifier.

The issue reported by the Checkstyle linter indicates that the final modifier is unnecessary for the parameter yml in the method declaration boolean isSchemaValid( final String schemaId, final String yml );. In Java, method parameters are implicitly final unless explicitly stated otherwise, meaning that they cannot be reassigned within the method. Therefore, including the final keyword for method parameters is redundant and can be removed to conform to the coding standards.

To fix the issue, we can simply remove the final modifier from the yml parameter. Here is the suggested code change:

Suggested change
boolean isSchemaValid( final String schemaId, final String yml );
boolean isSchemaValid( final String schemaId, String yml );

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;
}

}
Loading
Loading