From 2048c0f1acc5853574d8c2907c0998c56676bbe3 Mon Sep 17 00:00:00 2001 From: Jonathan Cobb Date: Tue, 1 Dec 2020 19:31:45 -0500 Subject: [PATCH] improve flexibility in mapping entities to open api schemas --- .../cobbzilla/wizard/model/OpenApiSchema.java | 12 +++++++ .../model/entityconfig/EntityConfig.java | 16 ++++++++-- .../entityconfig/EntityConfigSource.java | 4 ++- .../wizard/model/support/SupportInfo.java | 4 +-- .../wizard/model/search/SearchResults.java | 13 ++++---- .../AbstractEntityConfigsResource.java | 29 +++++++++-------- .../resources/AbstractTimezonesResource.java | 32 +++++++++++++++++-- .../server/config/OpenApiConfiguration.java | 7 ++-- 8 files changed, 86 insertions(+), 31 deletions(-) create mode 100644 wizard-common/src/main/java/org/cobbzilla/wizard/model/OpenApiSchema.java diff --git a/wizard-common/src/main/java/org/cobbzilla/wizard/model/OpenApiSchema.java b/wizard-common/src/main/java/org/cobbzilla/wizard/model/OpenApiSchema.java new file mode 100644 index 0000000..d409f3d --- /dev/null +++ b/wizard-common/src/main/java/org/cobbzilla/wizard/model/OpenApiSchema.java @@ -0,0 +1,12 @@ +package org.cobbzilla.wizard.model; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.RUNTIME) +public @interface OpenApiSchema { + + String[] exclude() default ""; + String[] include() default ""; + +} diff --git a/wizard-common/src/main/java/org/cobbzilla/wizard/model/entityconfig/EntityConfig.java b/wizard-common/src/main/java/org/cobbzilla/wizard/model/entityconfig/EntityConfig.java index 14e844a..bfd5afa 100644 --- a/wizard-common/src/main/java/org/cobbzilla/wizard/model/entityconfig/EntityConfig.java +++ b/wizard-common/src/main/java/org/cobbzilla/wizard/model/entityconfig/EntityConfig.java @@ -8,6 +8,7 @@ import lombok.extern.slf4j.Slf4j; import org.cobbzilla.util.reflect.ReflectionUtil; import org.cobbzilla.util.string.HasLocale; import org.cobbzilla.util.string.StringUtil; +import org.cobbzilla.wizard.model.OpenApiSchema; import org.cobbzilla.wizard.model.entityconfig.annotations.*; import org.cobbzilla.wizard.model.search.SqlViewField; import org.cobbzilla.wizard.validation.HasValue; @@ -190,11 +191,11 @@ public class EntityConfig { * non-empty values! */ public EntityConfig updateWithAnnotations() { - return updateWithAnnotations(getClassSafe(getClassName()), false); + return updateWithAnnotations(getClassSafe(getClassName()), false, null); } /** Update properties with values from the class' annotation. Doesn't override existing non-empty values! */ - public EntityConfig updateWithAnnotations(Class clazz, boolean isRootECCall) { + public EntityConfig updateWithAnnotations(Class clazz, boolean isRootECCall, OpenApiSchema schema) { if (isRootECCall && clazz == null) throw new NullPointerException("Root class cannot be null"); final Map fieldIndexes = new HashMap<>(); @@ -215,6 +216,17 @@ public class EntityConfig { updateWithAnnotation(clazz, clazz.getAnnotation(ECTypeURIs.class)); final Set entityFields = new HashSet<>(fieldNamesWithAnnotations(clazz, ECField.class, ECSearchable.class, ECForeignKey.class)); + if (schema != null) { + final boolean hasIncludes = !empty(schema.include()); + final boolean hasExcludes = !empty(schema.exclude()); + if (!hasIncludes && !hasExcludes) { + // include all getters + entityFields.addAll(ReflectionUtil.toMap(instantiate(clazz)).keySet()); + } else { + if (hasIncludes) entityFields.addAll(Arrays.asList(schema.include())); + if (hasExcludes) entityFields.removeAll(Arrays.asList(schema.exclude())); + } + } updateECFields(clazz, entityFields, fieldIndexes); updateWithAnnotation(clazz, clazz.getAnnotation(ECTypeChildren.class)); } diff --git a/wizard-common/src/main/java/org/cobbzilla/wizard/model/entityconfig/EntityConfigSource.java b/wizard-common/src/main/java/org/cobbzilla/wizard/model/entityconfig/EntityConfigSource.java index 6aecf20..a38b410 100644 --- a/wizard-common/src/main/java/org/cobbzilla/wizard/model/entityconfig/EntityConfigSource.java +++ b/wizard-common/src/main/java/org/cobbzilla/wizard/model/entityconfig/EntityConfigSource.java @@ -1,7 +1,9 @@ package org.cobbzilla.wizard.model.entityconfig; +import org.cobbzilla.wizard.model.OpenApiSchema; + public interface EntityConfigSource { EntityConfig getEntityConfig(Object thing); - EntityConfig getOrCreateEntityConfig(Object thing) throws Exception; + EntityConfig getOrCreateEntityConfig(Object thing, OpenApiSchema schema) throws Exception; } diff --git a/wizard-common/src/main/java/org/cobbzilla/wizard/model/support/SupportInfo.java b/wizard-common/src/main/java/org/cobbzilla/wizard/model/support/SupportInfo.java index ffb1585..2146930 100644 --- a/wizard-common/src/main/java/org/cobbzilla/wizard/model/support/SupportInfo.java +++ b/wizard-common/src/main/java/org/cobbzilla/wizard/model/support/SupportInfo.java @@ -1,14 +1,14 @@ package org.cobbzilla.wizard.model.support; import com.fasterxml.jackson.annotation.JsonIgnore; -import io.swagger.v3.oas.annotations.media.Schema; import lombok.Getter; import lombok.Setter; +import org.cobbzilla.wizard.model.OpenApiSchema; import java.util.HashMap; import java.util.Map; -@Schema +@OpenApiSchema public class SupportInfo extends BasicSupportInfo { @JsonIgnore @Getter @Setter private Map locale = new HashMap<>(); diff --git a/wizard-server/src/main/java/org/cobbzilla/wizard/model/search/SearchResults.java b/wizard-server/src/main/java/org/cobbzilla/wizard/model/search/SearchResults.java index 11c7a37..4da9770 100644 --- a/wizard-server/src/main/java/org/cobbzilla/wizard/model/search/SearchResults.java +++ b/wizard-server/src/main/java/org/cobbzilla/wizard/model/search/SearchResults.java @@ -2,7 +2,6 @@ package org.cobbzilla.wizard.model.search; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.databind.JavaType; -import io.swagger.v3.oas.annotations.media.Schema; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; @@ -10,7 +9,7 @@ import lombok.experimental.Accessors; import org.cobbzilla.util.json.JsonUtil; import org.cobbzilla.wizard.filters.Scrubbable; import org.cobbzilla.wizard.filters.ScrubbableField; -import org.cobbzilla.wizard.model.entityconfig.annotations.ECField; +import org.cobbzilla.wizard.model.OpenApiSchema; import java.util.ArrayList; import java.util.List; @@ -20,7 +19,7 @@ import java.util.concurrent.ConcurrentHashMap; import static org.cobbzilla.util.daemon.ZillaRuntime.die; import static org.cobbzilla.util.daemon.ZillaRuntime.empty; -@NoArgsConstructor @Accessors(chain=true) @Schema +@NoArgsConstructor @Accessors(chain=true) @OpenApiSchema public class SearchResults implements Scrubbable { public static final ScrubbableField[] SCRUBBABLE_FIELDS = new ScrubbableField[]{ @@ -44,10 +43,10 @@ public class SearchResults implements Scrubbable { return type; } - @ECField @Getter @Setter private List results = new ArrayList<>(); - @ECField @Getter @Setter private Integer totalCount; - @ECField @Getter @Setter private String nextPage; - @ECField @Getter @Setter private String error; + @Getter @Setter private List results = new ArrayList<>(); + @Getter @Setter private Integer totalCount; + @Getter @Setter private String nextPage; + @Getter @Setter private String error; public String getResultType() { return empty(results) ? null : results.get(0).getClass().getName(); } public void setResultType (String val) {} // noop diff --git a/wizard-server/src/main/java/org/cobbzilla/wizard/resources/AbstractEntityConfigsResource.java b/wizard-server/src/main/java/org/cobbzilla/wizard/resources/AbstractEntityConfigsResource.java index a52e5fe..234ea20 100644 --- a/wizard-server/src/main/java/org/cobbzilla/wizard/resources/AbstractEntityConfigsResource.java +++ b/wizard-server/src/main/java/org/cobbzilla/wizard/resources/AbstractEntityConfigsResource.java @@ -17,6 +17,7 @@ import org.cobbzilla.util.string.StringUtil; import org.cobbzilla.wizard.dao.AbstractCRUDDAO; import org.cobbzilla.wizard.dao.DAO; import org.cobbzilla.wizard.model.Identifiable; +import org.cobbzilla.wizard.model.OpenApiSchema; import org.cobbzilla.wizard.model.entityconfig.EntityConfig; import org.cobbzilla.wizard.model.entityconfig.EntityConfigSource; import org.cobbzilla.wizard.model.entityconfig.EntityFieldConfig; @@ -75,17 +76,15 @@ public abstract class AbstractEntityConfigsResource implements EntityConfigSourc @GET @Operation(security=@SecurityRequirement(name=SEC_API_KEY), - tags={API_TAG_UTILITY}, + tags=API_TAG_UTILITY, summary="Read entity configs. Returns an array of Strings, each an entity type. When param 'full' is true, response is a Map of entity type names to the full EntityConfig object for each type.", description="Read entity configs. Returns an array of Strings, each an entity type. When param 'full' is true, response is a Map of entity type names to the full EntityConfig object for each type.", - parameters={@Parameter(name="full", description="return all configs")}, - responses={@ApiResponse(responseCode=SC_OK, description="the name of the entity types, or a map of all configs", - content={@Content(mediaType=APPLICATION_JSON, examples={ + parameters=@Parameter(name="full", description="return all configs"), + responses=@ApiResponse(responseCode=SC_OK, description="the name of the entity types, or a map of all configs", + content=@Content(mediaType=APPLICATION_JSON, examples={ @ExampleObject(name="an array of entity type names", value="[\"SomeEntity\", \"AnotherEntity\"]"), @ExampleObject(name="when 'full' param is passed, returns map of name->config", value="{\"SomeEntity\": {\"entity-config-fields\": \"would-be-here\"}, \"AnotherEntity\": {\"entity-config-fields\": \"would-be-here\"}}") - } - )}) - } + })) ) public Response getConfigs(@Context ContainerRequest ctx, @QueryParam("full") Boolean full) { @@ -93,9 +92,9 @@ public abstract class AbstractEntityConfigsResource implements EntityConfigSourc return ok(full != null && full ? getConfigs().getEntries() : getConfigs().get().keySet()); } - @Override public EntityConfig getOrCreateEntityConfig(Object thing) throws Exception { + @Override public EntityConfig getOrCreateEntityConfig(Object thing, OpenApiSchema schema) throws Exception { final EntityConfig entityConfig = getEntityConfig(thing); - return entityConfig != null ? entityConfig : getEntityConfig(toClass(thing), false); + return entityConfig != null ? entityConfig : getEntityConfig(toClass(thing), false, schema); } @Override public EntityConfig getEntityConfig(Object thing) { @@ -118,13 +117,13 @@ public abstract class AbstractEntityConfigsResource implements EntityConfigSourc @GET @Path("/{name}") @Operation(security=@SecurityRequirement(name=SEC_API_KEY), - tags={API_TAG_UTILITY}, + tags=API_TAG_UTILITY, summary="Read the entity config for an entity type. Type names are case-insensitive.", description="Read the entity config for an entity type. Type names are case-insensitive.", - parameters={@Parameter(name="name", description="name of the entity type. names are case-insensitive.")}, + parameters=@Parameter(name="name", description="name of the entity type. names are case-insensitive."), responses={ @ApiResponse(responseCode=SC_OK, description="the EntityConfig object for the type"), - @ApiResponse(responseCode=SC_NOT_FOUND, description = "no EntityConfig exists with the name given") + @ApiResponse(responseCode=SC_NOT_FOUND, description="no EntityConfig exists with the name given") } ) public Response getConfig (@Context ContainerRequest ctx, @@ -212,6 +211,10 @@ public abstract class AbstractEntityConfigsResource implements EntityConfigSourc private EntityConfig getEntityConfig(Class clazz) throws Exception { return getEntityConfig(clazz, true); } private EntityConfig getEntityConfig(Class clazz, boolean root) throws Exception { + return getEntityConfig(clazz, root, null); + } + + private EntityConfig getEntityConfig(Class clazz, boolean root, OpenApiSchema schema) throws Exception { EntityConfig entityConfig; try { final InputStream in = loadResourceAsStream(ENTITY_CONFIG_BASE + "/" + packagePath(clazz) + "/" + @@ -225,7 +228,7 @@ public abstract class AbstractEntityConfigsResource implements EntityConfigSourc entityConfig.setClassName(clazz.getName()); try { - final EntityConfig updated = entityConfig.updateWithAnnotations(clazz, root); + final EntityConfig updated = entityConfig.updateWithAnnotations(clazz, root, schema); DAO dao = null; try { diff --git a/wizard-server/src/main/java/org/cobbzilla/wizard/resources/AbstractTimezonesResource.java b/wizard-server/src/main/java/org/cobbzilla/wizard/resources/AbstractTimezonesResource.java index 5210dca..9be358a 100644 --- a/wizard-server/src/main/java/org/cobbzilla/wizard/resources/AbstractTimezonesResource.java +++ b/wizard-server/src/main/java/org/cobbzilla/wizard/resources/AbstractTimezonesResource.java @@ -1,6 +1,10 @@ package org.cobbzilla.wizard.resources; import com.fasterxml.jackson.annotation.JsonCreator; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.cobbzilla.util.collection.NameAndValue; @@ -8,16 +12,22 @@ import org.cobbzilla.util.time.UnicodeTimezone; import javax.ws.rs.*; import javax.ws.rs.core.Response; -import java.util.*; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; import java.util.function.Function; import static java.util.function.Function.identity; import static java.util.stream.Collectors.toCollection; import static javax.ws.rs.core.MediaType.APPLICATION_JSON; import static org.cobbzilla.util.collection.NameAndValue.NAME_COMPARATOR; +import static org.cobbzilla.util.http.HttpStatusCodes.SC_NOT_FOUND; +import static org.cobbzilla.util.http.HttpStatusCodes.SC_OK; import static org.cobbzilla.util.time.UnicodeTimezone.getUnicodeTimezoneMap; import static org.cobbzilla.wizard.resources.ResourceUtil.notFound; import static org.cobbzilla.wizard.resources.ResourceUtil.ok; +import static org.cobbzilla.wizard.server.config.OpenApiConfiguration.API_TAG_UTILITY; +import static org.cobbzilla.wizard.server.config.OpenApiConfiguration.SEC_API_KEY; @Slf4j @Consumes(APPLICATION_JSON) @@ -71,8 +81,14 @@ public class AbstractTimezonesResource { = getUnicodeTimezoneMap().keySet().stream().map(TzFormat.dll::format).collect(toCollection(TreeSet::new)); @GET - public Response findAll (@PathParam("id") String id, - @QueryParam("format") TzFormat format) { + @Operation(security=@SecurityRequirement(name=SEC_API_KEY), + tags=API_TAG_UTILITY, + summary="List all time zones", + description="List all time zones. The format parameter determines the format of the JSON, see TzFormat enum", + parameters=@Parameter(name="format", description="format of the response"), + responses=@ApiResponse(responseCode=SC_OK, description="a JSON array of timezones in the format requested") + ) + public Response findAll (@QueryParam("format") TzFormat format) { final TzFormat fmt = format != null ? format : TzFormat.raw; switch (fmt) { case full: return ok(all_full); @@ -85,6 +101,16 @@ public class AbstractTimezonesResource { } @GET @Path("/{id: .*}") + @Operation(security=@SecurityRequirement(name=SEC_API_KEY), + tags=API_TAG_UTILITY, + summary="Get the canonical name for a time zone", + description="Get the canonical name for a time zone. Some time zones have alias names. This returns the canonical name.", + parameters=@Parameter(name="id", description="time zone name"), + responses={ + @ApiResponse(responseCode=SC_OK, description="the canonical name of the time zone"), + @ApiResponse(responseCode=SC_NOT_FOUND, description="the time zone was not found") + } + ) public Response find (@PathParam("id") String id) { UnicodeTimezone utz = UnicodeTimezone.fromString(id); diff --git a/wizard-server/src/main/java/org/cobbzilla/wizard/server/config/OpenApiConfiguration.java b/wizard-server/src/main/java/org/cobbzilla/wizard/server/config/OpenApiConfiguration.java index 34e1dc7..761f756 100644 --- a/wizard-server/src/main/java/org/cobbzilla/wizard/server/config/OpenApiConfiguration.java +++ b/wizard-server/src/main/java/org/cobbzilla/wizard/server/config/OpenApiConfiguration.java @@ -2,7 +2,6 @@ package org.cobbzilla.wizard.server.config; import com.github.jknack.handlebars.Handlebars; import io.swagger.v3.jaxrs2.integration.resources.OpenApiResource; -import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.integration.SwaggerConfiguration; import io.swagger.v3.oas.models.Components; import io.swagger.v3.oas.models.OpenAPI; @@ -18,6 +17,7 @@ import org.apache.commons.collections4.map.SingletonMap; import org.cobbzilla.util.handlebars.HandlebarsUtil; import org.cobbzilla.util.handlebars.HasHandlebars; import org.cobbzilla.wizard.filters.auth.AuthFilter; +import org.cobbzilla.wizard.model.OpenApiSchema; import org.cobbzilla.wizard.model.entityconfig.EntityConfig; import org.cobbzilla.wizard.model.entityconfig.EntityConfigSource; import org.cobbzilla.wizard.util.ClasspathScanner; @@ -111,7 +111,7 @@ public class OpenApiConfiguration { rc.register(new OpenApiResource().openApiConfiguration(oasConfig)); } - public static final AnnotationTypeFilter SCHEMA_FILTER = new AnnotationTypeFilter(Schema.class); + public static final AnnotationTypeFilter SCHEMA_FILTER = new AnnotationTypeFilter(OpenApiSchema.class); protected void addEntitySchemas(OpenAPI oas, String[] packages, RestServerConfiguration configuration) throws Exception { final EntityConfigSource entityConfigSource = configuration.getBean(EntityConfigSource.class); @@ -123,7 +123,8 @@ public class OpenApiConfiguration { .setFilter(SCHEMA_FILTER) .scan()); for (Class entity : apiEntities) { - final EntityConfig entityConfig = entityConfigSource.getOrCreateEntityConfig(entity); + final OpenApiSchema schema = entity.getAnnotation(OpenApiSchema.class); + final EntityConfig entityConfig = entityConfigSource.getOrCreateEntityConfig(entity, schema); oas.schema(entityConfig.example().getClass().getSimpleName(), entityConfig.openApiSchema()); } }