blob: 2c38e431e9b3e181f070ffa191cc2f367c0805a1 [file] [log] [blame]
/*
* 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.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.JsonNumber;
import javax.json.JsonString;
import javax.json.JsonValue;
import javax.json.bind.annotation.JsonbProperty;
import javax.json.bind.annotation.JsonbTransient;
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.databind.ser.std.StdScalarSerializer;
import com.fasterxml.jackson.databind.ser.std.StringSerializer;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import org.apache.geronimo.microprofile.openapi.impl.loader.ApiBindings;
import org.eclipse.microprofile.openapi.models.OpenAPI;
import org.eclipse.microprofile.openapi.models.headers.Header;
import org.eclipse.microprofile.openapi.models.media.Encoding;
import org.eclipse.microprofile.openapi.models.media.Schema;
import org.eclipse.microprofile.openapi.models.parameters.Parameter;
import org.eclipse.microprofile.openapi.models.security.SecurityScheme;
@Vetoed
public final class Yaml {
private Yaml() {
// no-op
}
public static OpenAPI loadAPI(final InputStream stream) {
try {
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();
ApiBindings.get().forEach((k, v) -> resolver.addMapping(Class.class.cast(k), v));
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);
}
}
});
module.addSerializer(JsonString.class, new StdScalarSerializer<JsonString>(JsonString.class) {
@Override
public void serialize(final JsonString value, final JsonGenerator gen, final SerializerProvider provider) throws IOException {
gen.writeString(value.getString());
}
});
module.addSerializer(JsonNumber.class, new StdScalarSerializer<JsonNumber>(JsonNumber.class) {
@Override
public void serialize(final JsonNumber value, final JsonGenerator gen, final SerializerProvider provider) throws IOException {
final double v = value.doubleValue();
if (v == value.intValue()) {
gen.writeNumber(value.intValue());
} else if (v == value.longValue()) {
gen.writeNumber(value.longValue());
} else {
gen.writeNumber(v);
}
}
});
module.addSerializer(JsonValue.class, new StdScalarSerializer<JsonValue>(JsonValue.class) {
@Override
public void serialize(final JsonValue value, final JsonGenerator gen, final SerializerProvider provider) throws IOException {
switch (value.getValueType()) {
case TRUE:
case FALSE:
gen.writeBoolean(value.equals(JsonValue.TRUE));
break;
case ARRAY:
case OBJECT:
gen.writeTree(new ObjectMapper().readTree(value.toString())); // not elegant but is it that common?
break;
default:
}
}
});
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
public Boolean hasAnyGetter(final Annotated a) {
return "getExtensions".equals(a.getName());
}
@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());
}
});
}
}