Skip to content
Permalink
Browse files
GERONIMO-6659 ensure yaml is set as default when relevant + GERONIMO-…
…6660 fixing yaml serializer
  • Loading branch information
rmannibucau committed Nov 30, 2018
1 parent e2bcfad commit 8ad36a0fec4c1f27bbfdd524e74505394c0b35bd
Showing 6 changed files with 375 additions and 79 deletions.
@@ -18,6 +18,7 @@

import static java.util.Optional.ofNullable;
import static java.util.stream.Collectors.toSet;
import static javax.ws.rs.core.MediaType.APPLICATION_JSON_TYPE;

import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
@@ -31,24 +32,30 @@

import javax.enterprise.event.Observes;
import javax.enterprise.inject.Instance;
import javax.enterprise.inject.spi.AfterDeploymentValidation;
import javax.enterprise.inject.spi.Annotated;
import javax.enterprise.inject.spi.AnnotatedMethod;
import javax.enterprise.inject.spi.Bean;
import javax.enterprise.inject.spi.BeanManager;
import javax.enterprise.inject.spi.BeforeBeanDiscovery;
import javax.enterprise.inject.spi.CDI;
import javax.enterprise.inject.spi.Extension;
import javax.enterprise.inject.spi.ProcessAnnotatedType;
import javax.enterprise.inject.spi.ProcessBean;
import javax.ws.rs.Path;
import javax.ws.rs.core.Application;
import javax.ws.rs.core.MediaType;

import org.apache.geronimo.microprofile.openapi.config.GeronimoOpenAPIConfig;
import org.apache.geronimo.microprofile.openapi.impl.filter.FilterImpl;
import org.apache.geronimo.microprofile.openapi.impl.loader.DefaultLoader;
import org.apache.geronimo.microprofile.openapi.impl.loader.yaml.Yaml;
import org.apache.geronimo.microprofile.openapi.impl.model.PathsImpl;
import org.apache.geronimo.microprofile.openapi.impl.processor.AnnotatedMethodElement;
import org.apache.geronimo.microprofile.openapi.impl.processor.AnnotatedTypeElement;
import org.apache.geronimo.microprofile.openapi.impl.processor.AnnotationProcessor;
import org.apache.geronimo.microprofile.openapi.jaxrs.JacksonOpenAPIYamlBodyWriter;
import org.apache.geronimo.microprofile.openapi.jaxrs.OpenAPIFilter;
import org.eclipse.microprofile.openapi.OASConfig;
import org.eclipse.microprofile.openapi.OASFilter;
import org.eclipse.microprofile.openapi.OASModelReader;
@@ -67,6 +74,7 @@ public class GeronimoOpenAPIExtension implements Extension {
private Collection<String> packages;
private Collection<String> excludePackages;
private Collection<String> excludeClasses;
private boolean jacksonIsPresent;

void init(@Observes final BeforeBeanDiscovery beforeBeanDiscovery) {
config = GeronimoOpenAPIConfig.create();
@@ -76,6 +84,18 @@ void init(@Observes final BeforeBeanDiscovery beforeBeanDiscovery) {
packages = getConfigCollection(OASConfig.SCAN_PACKAGES);
excludePackages = getConfigCollection(OASConfig.SCAN_EXCLUDE_PACKAGES);
excludeClasses = getConfigCollection(OASConfig.SCAN_EXCLUDE_CLASSES);
try {
Yaml.getObjectMapper();
jacksonIsPresent = true;
} catch (final Error | RuntimeException e) {
// no-op
}
}

void vetoJacksonIfNotHere(@Observes final ProcessAnnotatedType<JacksonOpenAPIYamlBodyWriter> event) {
if (!jacksonIsPresent) {
event.veto();
}
}

<T> void findEndpointsAndApplication(@Observes final ProcessBean<T> event) {
@@ -87,6 +107,14 @@ <T> void findEndpointsAndApplication(@Observes final ProcessBean<T> event) {
}
}

void afterValidation(@Observes final AfterDeploymentValidation validation,
final BeanManager beanManager) {
final OpenAPIFilter filter = OpenAPIFilter.class.cast(
beanManager.getReference(beanManager.resolve(beanManager.getBeans(OpenAPIFilter.class)),
OpenAPIFilter.class, beanManager.createCreationalContext(null)));
filter.setDefaultMediaType(jacksonIsPresent ? new MediaType("text", "vnd.yaml") : APPLICATION_JSON_TYPE);
}

public OpenAPI getOrCreateOpenAPI(final Application application) {
if (classes != null) {
final ClassLoader loader = Thread.currentThread().getContextClassLoader();
@@ -16,11 +16,15 @@
*/
package org.apache.geronimo.microprofile.openapi.impl.loader.yaml;

import static java.util.Optional.ofNullable;

import java.io.IOException;
import java.io.InputStream;
import java.math.BigDecimal;
import java.util.stream.Stream;

import javax.enterprise.inject.Vetoed;
import javax.json.bind.annotation.JsonbProperty;
import javax.json.bind.annotation.JsonbTransient;

import org.apache.geronimo.microprofile.openapi.impl.model.APIResponseImpl;
@@ -92,15 +96,21 @@
import org.eclipse.microprofile.openapi.models.servers.ServerVariables;
import org.eclipse.microprofile.openapi.models.tags.Tag;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyName;
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.introspect.Annotated;
import com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector;
import com.fasterxml.jackson.databind.module.SimpleAbstractTypeResolver;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.std.NumberSerializer;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;

@Vetoed
@@ -111,86 +121,146 @@ private Yaml() {

public static OpenAPI loadAPI(final InputStream stream) {
try {
final SimpleAbstractTypeResolver resolver = new SimpleAbstractTypeResolver();
resolver.addMapping(APIResponse.class, APIResponseImpl.class);
resolver.addMapping(APIResponses.class, APIResponsesImpl.class);
resolver.addMapping(Callback.class, CallbackImpl.class);
resolver.addMapping(Components.class, ComponentsImpl.class);
resolver.addMapping(Contact.class, ContactImpl.class);
resolver.addMapping(Content.class, ContentImpl.class);
resolver.addMapping(Discriminator.class, DiscriminatorImpl.class);
resolver.addMapping(Encoding.class, EncodingImpl.class);
resolver.addMapping(Example.class, ExampleImpl.class);
resolver.addMapping(Extensible.class, ExtensibleImpl.class);
resolver.addMapping(ExternalDocumentation.class, ExternalDocumentationImpl.class);
resolver.addMapping(Header.class, HeaderImpl.class);
resolver.addMapping(Info.class, InfoImpl.class);
resolver.addMapping(License.class, LicenseImpl.class);
resolver.addMapping(Link.class, LinkImpl.class);
resolver.addMapping(MediaType.class, MediaTypeImpl.class);
resolver.addMapping(OAuthFlow.class, OAuthFlowImpl.class);
resolver.addMapping(OAuthFlows.class, OAuthFlowsImpl.class);
resolver.addMapping(OpenAPI.class, OpenAPIImpl.class);
resolver.addMapping(Operation.class, OperationImpl.class);
resolver.addMapping(Parameter.class, ParameterImpl.class);
resolver.addMapping(PathItem.class, PathItemImpl.class);
resolver.addMapping(Paths.class, PathsImpl.class);
resolver.addMapping(Reference.class, ReferenceImpl.class);
resolver.addMapping(RequestBody.class, RequestBodyImpl.class);
resolver.addMapping(Schema.class, SchemaImpl.class);
resolver.addMapping(Scopes.class, ScopesImpl.class);
resolver.addMapping(SecurityRequirement.class, SecurityRequirementImpl.class);
resolver.addMapping(SecurityScheme.class, SecuritySchemeImpl.class);
resolver.addMapping(Server.class, ServerImpl.class);
resolver.addMapping(ServerVariable.class, ServerVariableImpl.class);
resolver.addMapping(ServerVariables.class, ServerVariablesImpl.class);
resolver.addMapping(Tag.class, TagImpl.class);
resolver.addMapping(XML.class, XMLImpl.class);

final SimpleModule module = new SimpleModule();
module.setAbstractTypes(resolver);
module.addDeserializer(Parameter.In.class, new JsonDeserializer<Parameter.In>() {
@Override
public Parameter.In deserialize(final JsonParser p, final DeserializationContext ctxt) {
return Stream.of(Parameter.In.values()).filter(it -> {
try {
return it.name().equalsIgnoreCase(p.getValueAsString());
} catch (IOException e) {
throw new IllegalArgumentException(e);
}
}).findFirst().orElseThrow(() -> new IllegalArgumentException("No matching In value"));
}
});
module.addDeserializer(Schema.SchemaType.class, new JsonDeserializer<Schema.SchemaType>() {
@Override
public Schema.SchemaType deserialize(final JsonParser p, final DeserializationContext ctxt) {
return Stream.of(Schema.SchemaType.values()).filter(it -> {
try {
return it.name().equalsIgnoreCase(p.getValueAsString());
} catch (IOException e) {
throw new IllegalArgumentException(e);
}
}).findFirst().orElseThrow(() -> new IllegalArgumentException("No matching SchemaType value"));
}
});

final ObjectMapper mapper = new ObjectMapper(new YAMLFactory());
mapper.registerModule(module);
mapper.setAnnotationIntrospector(new JacksonAnnotationIntrospector() {
@Override
protected boolean _isIgnorable(final Annotated a) {
return super._isIgnorable(a) || a.getAnnotation(JsonbTransient.class) != null;
}
});
mapper.setPropertyNamingStrategy(new PropertyNamingStrategy.PropertyNamingStrategyBase() {
@Override
public String translate(final String propertyName) {
return "ref".equals(propertyName) ? "$ref" : propertyName;
}
});
final ObjectMapper mapper = getObjectMapper();
return mapper.readValue(stream, OpenAPI.class);
} catch (final IOException e) {
throw new IllegalArgumentException(e);
}
}

// let be reusable in integrations
public static ObjectMapper getObjectMapper() {
final SimpleAbstractTypeResolver resolver = new SimpleAbstractTypeResolver();
resolver.addMapping(APIResponse.class, APIResponseImpl.class);
resolver.addMapping(APIResponses.class, APIResponsesImpl.class);
resolver.addMapping(Callback.class, CallbackImpl.class);
resolver.addMapping(Components.class, ComponentsImpl.class);
resolver.addMapping(Contact.class, ContactImpl.class);
resolver.addMapping(Content.class, ContentImpl.class);
resolver.addMapping(Discriminator.class, DiscriminatorImpl.class);
resolver.addMapping(Encoding.class, EncodingImpl.class);
resolver.addMapping(Example.class, ExampleImpl.class);
resolver.addMapping(Extensible.class, ExtensibleImpl.class);
resolver.addMapping(ExternalDocumentation.class, ExternalDocumentationImpl.class);
resolver.addMapping(Header.class, HeaderImpl.class);
resolver.addMapping(Info.class, InfoImpl.class);
resolver.addMapping(License.class, LicenseImpl.class);
resolver.addMapping(Link.class, LinkImpl.class);
resolver.addMapping(MediaType.class, MediaTypeImpl.class);
resolver.addMapping(OAuthFlow.class, OAuthFlowImpl.class);
resolver.addMapping(OAuthFlows.class, OAuthFlowsImpl.class);
resolver.addMapping(OpenAPI.class, OpenAPIImpl.class);
resolver.addMapping(Operation.class, OperationImpl.class);
resolver.addMapping(Parameter.class, ParameterImpl.class);
resolver.addMapping(PathItem.class, PathItemImpl.class);
resolver.addMapping(Paths.class, PathsImpl.class);
resolver.addMapping(Reference.class, ReferenceImpl.class);
resolver.addMapping(RequestBody.class, RequestBodyImpl.class);
resolver.addMapping(Schema.class, SchemaImpl.class);
resolver.addMapping(Scopes.class, ScopesImpl.class);
resolver.addMapping(SecurityRequirement.class, SecurityRequirementImpl.class);
resolver.addMapping(SecurityScheme.class, SecuritySchemeImpl.class);
resolver.addMapping(Server.class, ServerImpl.class);
resolver.addMapping(ServerVariable.class, ServerVariableImpl.class);
resolver.addMapping(ServerVariables.class, ServerVariablesImpl.class);
resolver.addMapping(Tag.class, TagImpl.class);
resolver.addMapping(XML.class, XMLImpl.class);

final SimpleModule module = new SimpleModule();
module.setAbstractTypes(resolver);
module.addDeserializer(Parameter.In.class, new JsonDeserializer<Parameter.In>() {
@Override
public Parameter.In deserialize(final JsonParser p, final DeserializationContext ctxt) {
return Stream.of(Parameter.In.values()).filter(it -> {
try {
return it.name().equalsIgnoreCase(p.getValueAsString());
} catch (IOException e) {
throw new IllegalArgumentException(e);
}
}).findFirst().orElseThrow(() -> new IllegalArgumentException("No matching In value"));
}
});
module.addDeserializer(Schema.SchemaType.class, new JsonDeserializer<Schema.SchemaType>() {
@Override
public Schema.SchemaType deserialize(final JsonParser p, final DeserializationContext ctxt) {
return Stream.of(Schema.SchemaType.values()).filter(it -> {
try {
return it.name().equalsIgnoreCase(p.getValueAsString());
} catch (IOException e) {
throw new IllegalArgumentException(e);
}
}).findFirst().orElseThrow(() -> new IllegalArgumentException("No matching SchemaType value"));
}
});
module.addDeserializer(SecurityScheme.Type.class, new JsonDeserializer<SecurityScheme.Type>() {
@Override
public SecurityScheme.Type deserialize(final JsonParser p, final DeserializationContext ctxt) {
return Stream.of(SecurityScheme.Type.values()).filter(it -> {
try {
return it.name().equalsIgnoreCase(p.getValueAsString());
} catch (IOException e) {
throw new IllegalArgumentException(e);
}
}).findFirst().orElseThrow(() -> new IllegalArgumentException("No matching SecurityScheme.Type value"));
}
});
module.addSerializer(BigDecimal.class, new NumberSerializer(BigDecimal.class) {
@Override
public void serialize(final Number value, final JsonGenerator g,
final SerializerProvider provider) throws IOException {
if (BigDecimal.class.isInstance(value) && value.doubleValue() == value.longValue()) {
super.serialize(value.longValue(), g, provider);
} else {
super.serialize(value, g, provider);
}
}
});
Stream.of(SecurityScheme.Type.class, SecurityScheme.In.class,
Schema.SchemaType.class, Header.Style.class, Encoding.Style.class,
Parameter.Style.class, Parameter.In.class)
.forEach(it -> toStringSerializer(module, it));

final ObjectMapper mapper = new ObjectMapper(new YAMLFactory());
mapper.registerModule(module);
mapper.setAnnotationIntrospector(new JacksonAnnotationIntrospector() {
@Override
protected boolean _isIgnorable(final Annotated a) {
return super._isIgnorable(a) || a.getAnnotation(JsonbTransient.class) != null;
}

@Override
public PropertyName findNameForSerialization(final Annotated a) {
return ofNullable(a.getAnnotation(JsonbProperty.class))
.map(JsonbProperty::value)
.map(PropertyName::new)
.orElseGet(() -> super.findNameForSerialization(a));
}

@Override
public PropertyName findNameForDeserialization(final Annotated a) {
return ofNullable(a.getAnnotation(JsonbProperty.class))
.map(JsonbProperty::value)
.map(PropertyName::new)
.orElseGet(() -> super.findNameForDeserialization(a));
}
});
mapper.setPropertyNamingStrategy(new PropertyNamingStrategy.PropertyNamingStrategyBase() {
@Override
public String translate(final String propertyName) {
return "ref".equals(propertyName) ? "$ref" : propertyName;
}
});
return mapper.setDefaultPropertyInclusion(JsonInclude.Include.NON_NULL);
}

private static <T> void toStringSerializer(final SimpleModule module, final Class<T> it) {
module.addSerializer(it, new JsonSerializer<T>() {
@Override
public void serialize(final T value,
final JsonGenerator gen,
final SerializerProvider serializers) throws IOException {
gen.writeString(value.toString());
}
});
}
}
@@ -0,0 +1,39 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.geronimo.microprofile.openapi.jaxrs;

import java.lang.annotation.Annotation;
import java.lang.reflect.Type;

import javax.ws.rs.core.MediaType;
import javax.ws.rs.ext.MessageBodyWriter;

// potential base to use snakeyaml or another serializer
public abstract class BaseOpenAPIYamlBodyWriter<T> implements MessageBodyWriter<T> {
@Override
public boolean isWriteable(final Class<?> type, final Type genericType,
final Annotation[] annotations, final MediaType mediaType) {
return type.getName().startsWith("org.apache.geronimo.microprofile.openapi.impl.model") ||
type.getName().startsWith("org.eclipse.microprofile.openapi.models");
}

@Override
public long getSize(final T t, final Class<?> type, final Type genericType,
final Annotation[] annotations, final MediaType mediaType) {
return -1;
}
}

0 comments on commit 8ad36a0

Please sign in to comment.