@@ -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 ""; | |||
} |
@@ -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<String, Integer> fieldIndexes = new HashMap<>(); | |||
@@ -215,6 +216,17 @@ public class EntityConfig { | |||
updateWithAnnotation(clazz, clazz.getAnnotation(ECTypeURIs.class)); | |||
final Set<String> 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)); | |||
} | |||
@@ -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; | |||
} |
@@ -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<String, BasicSupportInfo> locale = new HashMap<>(); | |||
@@ -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<E> implements Scrubbable { | |||
public static final ScrubbableField[] SCRUBBABLE_FIELDS = new ScrubbableField[]{ | |||
@@ -44,10 +43,10 @@ public class SearchResults<E> implements Scrubbable { | |||
return type; | |||
} | |||
@ECField @Getter @Setter private List<E> 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<E> 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 | |||
@@ -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 { | |||
@@ -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); | |||
@@ -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()); | |||
} | |||
} | |||