Skip to content

Commit

Permalink
Feature/add generation of async api (#71)
Browse files Browse the repository at this point in the history
* Add generate async api functionality

* Add e2e test collection

* Update sdk version to latest one

* Fix unit test for jsonschema

* Fix graal native image reflection und resource
  • Loading branch information
michelu89 authored Apr 19, 2024
1 parent 52c6527 commit b86c02c
Show file tree
Hide file tree
Showing 9 changed files with 586 additions and 54 deletions.
36 changes: 24 additions & 12 deletions aspect-model-editor-runtime/.graalvm/reflect-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -5634,6 +5634,18 @@
"boolean",
"java.util.Optional"
]
},
{
"name": "asyncApiSpec",
"parameterTypes": [
"java.lang.String",
"java.lang.String",
"java.lang.String",
"java.lang.String",
"java.lang.String",
"boolean",
"boolean"
]
}
]
},
Expand Down Expand Up @@ -13411,21 +13423,21 @@
},
{
"name": "sun.awt.X11FontManager",
"allDeclaredConstructors" : true,
"allPublicConstructors" : true,
"allDeclaredMethods" : true,
"allPublicMethods" : true,
"allDeclaredFields" : true,
"allPublicFields" : true
"allDeclaredConstructors": true,
"allPublicConstructors": true,
"allDeclaredMethods": true,
"allPublicMethods": true,
"allDeclaredFields": true,
"allPublicFields": true
},
{
"name": "sun.font.FontConfigManager",
"allDeclaredConstructors" : true,
"allPublicConstructors" : true,
"allDeclaredMethods" : true,
"allPublicMethods" : true,
"allDeclaredFields" : true,
"allPublicFields" : true
"allDeclaredConstructors": true,
"allPublicConstructors": true,
"allDeclaredMethods": true,
"allPublicMethods": true,
"allDeclaredFields": true,
"allPublicFields": true
},
{
"name": "sun.misc.Unsafe",
Expand Down
3 changes: 3 additions & 0 deletions aspect-model-editor-runtime/.graalvm/resource-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@
{
"pattern": "\\Qopenapi/OffsetBasedPaging.json\\E"
},
{
"pattern": "\\Qasyncapi/AsyncApiRootJson.json\\E"
},
{
"pattern": "\\Qcom/eclipsesource/v8/V8.class\\E"
},
Expand Down
315 changes: 305 additions & 10 deletions aspect-model-editor-runtime/postman/ame.postman_collection.json

Large diffs are not rendered by default.

7 changes: 5 additions & 2 deletions aspect-model-editor-service/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@
~ Copyright (c) 2023 Robert Bosch Manufacturing Solutions GmbH, Germany. All rights reserved.
-->

<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

Expand Down Expand Up @@ -71,6 +70,10 @@
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
</dependency>

<!-- Third party testing dependencies -->
<dependency>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,39 +16,52 @@
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;

import org.apache.commons.lang3.LocaleUtils;
import org.eclipse.esmf.ame.exceptions.FileHandlingException;
import org.eclipse.esmf.ame.exceptions.GenerationException;
import org.eclipse.esmf.ame.exceptions.InvalidAspectModelException;
import org.eclipse.esmf.ame.resolver.strategy.FileSystemStrategy;
import org.eclipse.esmf.ame.resolver.strategy.utils.ResolverUtils;
import org.eclipse.esmf.ame.services.utils.ZipUtils;
import org.eclipse.esmf.ame.utils.ModelUtils;
import org.eclipse.esmf.aspectmodel.aas.AasFileFormat;
import org.eclipse.esmf.aspectmodel.aas.AspectModelAasGenerator;
import org.eclipse.esmf.aspectmodel.generator.asyncapi.AspectModelAsyncApiGenerator;
import org.eclipse.esmf.aspectmodel.generator.asyncapi.AsyncApiSchemaArtifact;
import org.eclipse.esmf.aspectmodel.generator.asyncapi.AsyncApiSchemaGenerationConfig;
import org.eclipse.esmf.aspectmodel.generator.asyncapi.AsyncApiSchemaGenerationConfigBuilder;
import org.eclipse.esmf.aspectmodel.generator.docu.AspectModelDocumentationGenerator;
import org.eclipse.esmf.aspectmodel.generator.json.AspectModelJsonPayloadGenerator;
import org.eclipse.esmf.aspectmodel.generator.jsonschema.AspectModelJsonSchemaGenerator;
import org.eclipse.esmf.aspectmodel.generator.openapi.AspectModelOpenApiGenerator;
import org.eclipse.esmf.aspectmodel.generator.openapi.PagingOption;
import org.eclipse.esmf.aspectmodel.resolver.services.DataType;
import org.eclipse.esmf.aspectmodel.resolver.services.VersionedModel;
import org.eclipse.esmf.metamodel.Aspect;
import org.eclipse.esmf.metamodel.AspectContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator;
import com.fasterxml.jackson.dataformat.yaml.YAMLMapper;
import com.google.common.collect.ImmutableMap;

import io.vavr.control.Try;

@Service
public class GenerateService {
private static final Logger LOG = LoggerFactory.getLogger( GenerateService.class );

private static final ObjectMapper YAML_MAPPER = new YAMLMapper().enable( YAMLGenerator.Feature.MINIMIZE_QUOTES );
private static final String COULD_NOT_LOAD_ASPECT = "Could not load Aspect";
private static final String COULD_NOT_LOAD_ASPECT_MODEL = "Could not load Aspect model, please make sure the model is valid.";
public static final String WRONG_RESOURCE_PATH_ID = "The resource path ID and properties ID do not match. Please verify and correct them.";
Expand Down Expand Up @@ -90,8 +103,7 @@ public String jsonSchema( final String aspectModel, final String language ) {

public String sampleJSONPayload( final String aspectModel ) {
try {
return new AspectModelJsonPayloadGenerator(
generateAspectContext( aspectModel ) ).generateJson();
return new AspectModelJsonPayloadGenerator( generateAspectContext( aspectModel ) ).generateJson();
} catch ( final IOException e ) {
LOG.error( COULD_NOT_LOAD_ASPECT_MODEL );
throw new InvalidAspectModelException( COULD_NOT_LOAD_ASPECT, e );
Expand Down Expand Up @@ -144,30 +156,24 @@ private AspectContext generateAspectContext( final String aspectModel ) {
public String generateYamlOpenApiSpec( final String language, final String aspectModel, final String baseUrl,
final boolean includeQueryApi, final boolean useSemanticVersion, final Optional<PagingOption> pagingOption,
final Optional<String> resourcePath, final Optional<String> yamlProperties ) {
try {
final String ymlOutput = new AspectModelOpenApiGenerator().applyForYaml(
ResolverUtils.resolveAspectFromModel( aspectModel ),
useSemanticVersion, baseUrl, resourcePath, yamlProperties, includeQueryApi, pagingOption,
Locale.forLanguageTag( language ) );

if ( ymlOutput.equals( "--- {}\n" ) ) {
throw new GenerationException( WRONG_RESOURCE_PATH_ID );
}
final String ymlOutput = new AspectModelOpenApiGenerator().applyForYaml(
ResolverUtils.resolveAspectFromModel( aspectModel ), useSemanticVersion, baseUrl, resourcePath,
yamlProperties, includeQueryApi, pagingOption, Locale.forLanguageTag( language ) );

return ymlOutput;
} catch ( final IOException e ) {
LOG.error( "YAML OpenAPI specification could not be generated." );
throw new InvalidAspectModelException( "Error generating YAML OpenAPI specification", e );
if ( ymlOutput.equals( "--- {}\n" ) ) {
throw new GenerationException( WRONG_RESOURCE_PATH_ID );
}

return ymlOutput;
}

public String generateJsonOpenApiSpec( final String language, final String aspectModel, final String baseUrl,
final boolean includeQueryApi, final boolean useSemanticVersion, final Optional<PagingOption> pagingOption,
final Optional<String> resourcePath, final Optional<JsonNode> jsonProperties ) {
try {
final JsonNode json = new AspectModelOpenApiGenerator().applyForJson(
ResolverUtils.resolveAspectFromModel( aspectModel ), useSemanticVersion, baseUrl,
resourcePath, jsonProperties, includeQueryApi, pagingOption, LocaleUtils.toLocale( language ) );
ResolverUtils.resolveAspectFromModel( aspectModel ), useSemanticVersion, baseUrl, resourcePath,
jsonProperties, includeQueryApi, pagingOption, LocaleUtils.toLocale( language ) );

final ByteArrayOutputStream out = new ByteArrayOutputStream();
final ObjectMapper objectMapper = new ObjectMapper();
Expand All @@ -186,4 +192,83 @@ public String generateJsonOpenApiSpec( final String language, final String aspec
throw new InvalidAspectModelException( "Error generating JSON OpenAPI specification", e );
}
}

public byte[] generateAsyncApiSpec( final String aspectModel, final String language, final String output,
final String applicationId, final String channelAddress, final boolean useSemanticVersion,
final boolean writeSeparateFiles ) {
final AspectModelAsyncApiGenerator generator = new AspectModelAsyncApiGenerator();
final AsyncApiSchemaGenerationConfig config = buildAsyncApiSchemaGenerationConfig( applicationId, channelAddress,
useSemanticVersion, language );

final Aspect aspect = ResolverUtils.resolveAspectFromModel( aspectModel );
final AsyncApiSchemaArtifact asyncApiSpec = generator.apply( aspect, config );

if ( writeSeparateFiles ) {
return generateZipFile( asyncApiSpec, output );
}

return generateSingleFile( asyncApiSpec, output );
}

private AsyncApiSchemaGenerationConfig buildAsyncApiSchemaGenerationConfig( final String applicationId,
final String channelAddress, final boolean useSemanticVersion, final String language ) {
return AsyncApiSchemaGenerationConfigBuilder.builder().useSemanticVersion( useSemanticVersion )
.applicationId( applicationId ).channelAddress( channelAddress )
.locale( LocaleUtils.toLocale( language ) ).build();
}

private byte[] generateZipFile( final AsyncApiSchemaArtifact asyncApiSpec, final String output ) {
if ( output.equals( "json" ) ) {
return jsonZip( asyncApiSpec.getContentWithSeparateSchemasAsJson() );
}

return yamlZip( asyncApiSpec.getContentWithSeparateSchemasAsYaml() );
}

private byte[] jsonZip( final Map<Path, JsonNode> separateFilesContent ) {
final ObjectMapper objectMapper = new ObjectMapper();
final Map<Path, byte[]> content = new HashMap<>();

for ( final Map.Entry<Path, JsonNode> entry : separateFilesContent.entrySet() ) {
try {
final byte[] bytes = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsBytes( entry.getValue() );
content.put( entry.getKey(), bytes );
} catch ( final JsonProcessingException e ) {
LOG.error( "Failed to convert JSON to bytes.", e );
throw new FileHandlingException( "Failed to get JSON async api.", e );
}
}

return ZipUtils.createAsyncApiPackage( content );
}

private byte[] yamlZip( final Map<Path, String> separateFilesContent ) {
final Map<Path, byte[]> content = new HashMap<>();

for ( final Map.Entry<Path, String> entry : separateFilesContent.entrySet() ) {
final byte[] bytes = entry.getValue().getBytes( StandardCharsets.UTF_8 );
content.put( entry.getKey(), bytes );
}

return ZipUtils.createAsyncApiPackage( content );
}

private byte[] generateSingleFile( final AsyncApiSchemaArtifact asyncApiSpec, final String output ) {
final JsonNode json = asyncApiSpec.getContent();

if ( output.equals( "yaml" ) ) {
return jsonToYaml( json ).getBytes( StandardCharsets.UTF_8 );
}

return json.toString().getBytes( StandardCharsets.UTF_8 );
}

private String jsonToYaml( final JsonNode json ) {
try {
return YAML_MAPPER.writeValueAsString( json );
} catch ( final JsonProcessingException e ) {
LOG.error( "JSON could not be converted to YAML", e );
throw new FileHandlingException( "Failed to get YAML async api.", e );
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.HashSet;
Expand All @@ -43,14 +44,35 @@ private ZipUtils() {

static final int BUFFER = 1024;

public static byte[] createAsyncApiPackage( final Map<Path, byte[]> content ) {
final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();

try ( final ZipOutputStream zos = new ZipOutputStream( outputStream ) ) {
for ( final Map.Entry<Path, byte[]> entry : content.entrySet() ) {
final ZipEntry zipEntry = new ZipEntry( entry.getKey().toString() );
zos.putNextEntry( zipEntry );

final byte[] bytes = entry.getValue();
zos.write( bytes, 0, bytes.length );

zos.closeEntry();
}
} catch ( final IOException e ) {
LOG.error( "Failed to create the asynchronous API ZIP file." );
throw new CreateFileException( "An error occurred while creating the ZIP file for the async API.", e );
}

return outputStream.toByteArray();
}

public static byte[] createPackageFromCache( final Map<String, String> exportCache ) throws IOException {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();

try ( ZipOutputStream zos = new ZipOutputStream( outputStream ) ) {
Set<String> zipFolderSet = new HashSet<>();
Set<String> zipVersionedNamespaceSet = new HashSet<>();
try ( final ZipOutputStream zos = new ZipOutputStream( outputStream ) ) {
final Set<String> zipFolderSet = new HashSet<>();
final Set<String> zipVersionedNamespaceSet = new HashSet<>();

for ( Map.Entry<String, String> entry : exportCache.entrySet() ) {
for ( final Map.Entry<String, String> entry : exportCache.entrySet() ) {
final String[] fileStructure = entry.getKey().split( ":" );
final String aspectModel = entry.getValue();

Expand Down Expand Up @@ -85,7 +107,8 @@ public static void createPackageFromWorkspace( final String zipFileName, final S
final String storagePath ) throws IOException {
final String zipFile = aspectModelPath + File.separator + zipFileName;

try ( FileOutputStream fos = new FileOutputStream( zipFile ); ZipOutputStream zos = new ZipOutputStream( fos ) ) {
try ( final FileOutputStream fos = new FileOutputStream( zipFile );
final ZipOutputStream zos = new ZipOutputStream( fos ) ) {

final List<File> fileList = getFileList( new File( storagePath ), new ArrayList<>(), storagePath );

Expand Down
Loading

0 comments on commit b86c02c

Please sign in to comment.