diff --git a/wizard-common/pom.xml b/wizard-common/pom.xml index 26c54a2..17372ae 100644 --- a/wizard-common/pom.xml +++ b/wizard-common/pom.xml @@ -98,6 +98,23 @@ This code is available under the Apache License, version 2: http://www.apache.or 2.3.0 + + + io.swagger.core.v3 + swagger-jaxrs2 + ${swagger.version} + + + io.swagger.core.v3 + swagger-integration + ${swagger.version} + + + io.swagger.core.v3 + swagger-annotations + ${swagger.version} + + 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 b05f839..14e844a 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 @@ -1,5 +1,6 @@ package org.cobbzilla.wizard.model.entityconfig; +import io.swagger.v3.oas.models.media.Schema; import lombok.Getter; import lombok.Setter; import lombok.ToString; @@ -24,7 +25,11 @@ import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.*; +import static lombok.AccessLevel.PRIVATE; import static org.cobbzilla.util.daemon.ZillaRuntime.empty; +import static org.cobbzilla.util.daemon.ZillaRuntime.shortError; +import static org.cobbzilla.util.json.JsonUtil.NOTNULL_MAPPER; +import static org.cobbzilla.util.json.JsonUtil.json; import static org.cobbzilla.util.reflect.ReflectionUtil.*; import static org.cobbzilla.util.string.StringUtil.*; @@ -698,4 +703,45 @@ public class EntityConfig { } return validation; } + + // do not expose a getter for the example, we don't want this appearing in JSON + @Getter(lazy=true, value=PRIVATE) private final Object example = initExample(); + private Object initExample() { + final Object o = instantiate(className); + for (String field : fieldNames) { + try { + ReflectionUtil.set(o, field, fields.get(field).example()); + } catch (Exception e) { + log.info("initExample: error setting "+field+" on "+o.getClass().getName()+": "+shortError(e)); + } + } + return o; + } + + public T example() { return (T) getExample(); } + + public Schema openApiSchema() { + final T defaultObj = instantiate(className); + final T example = example(); + final Schema s = new Schema<>(); + + final String simpleName = defaultObj.getClass().getSimpleName(); + s.name(simpleName) + .title(camelCaseToString(simpleName)) + .example(json(example, NOTNULL_MAPPER)); + s.setDefault(defaultObj); + final Map props = new HashMap<>(); + final List required = new ArrayList<>(); + for (String field : fieldNames) { + final Schema fieldSchema = fieldToOpenApiSchema(this.getFields().get(field), required); + props.put(field, fieldSchema); + } + s.required(required).properties(props); + return s; + } + + private Schema fieldToOpenApiSchema(EntityFieldConfig fieldConfig, List required) { + if (fieldConfig.required()) required.add(fieldConfig.getDisplayName()); + return fieldConfig.openApiType(); + } } 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 34fc085..6aecf20 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 @@ -3,5 +3,5 @@ package org.cobbzilla.wizard.model.entityconfig; public interface EntityConfigSource { EntityConfig getEntityConfig(Object thing); - + EntityConfig getOrCreateEntityConfig(Object thing) throws Exception; } diff --git a/wizard-common/src/main/java/org/cobbzilla/wizard/model/entityconfig/EntityFieldConfig.java b/wizard-common/src/main/java/org/cobbzilla/wizard/model/entityconfig/EntityFieldConfig.java index 49e1c6c..1053758 100644 --- a/wizard-common/src/main/java/org/cobbzilla/wizard/model/entityconfig/EntityFieldConfig.java +++ b/wizard-common/src/main/java/org/cobbzilla/wizard/model/entityconfig/EntityFieldConfig.java @@ -1,6 +1,7 @@ package org.cobbzilla.wizard.model.entityconfig; import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.models.media.Schema; import lombok.Getter; import lombok.Setter; import lombok.ToString; @@ -9,15 +10,19 @@ import lombok.extern.slf4j.Slf4j; import org.cobbzilla.wizard.model.Identifiable; import org.cobbzilla.wizard.validation.ValidationResult; import org.cobbzilla.wizard.validation.Validator; +import org.joda.time.DateTime; import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.Map; +import static java.util.concurrent.TimeUnit.DAYS; +import static java.util.concurrent.TimeUnit.MINUTES; import static org.cobbzilla.util.daemon.ZillaRuntime.*; import static org.cobbzilla.util.json.JsonUtil.json; import static org.cobbzilla.util.reflect.ReflectionUtil.copy; +import static org.cobbzilla.util.reflect.ReflectionUtil.instantiate; import static org.cobbzilla.util.string.StringUtil.camelCaseToString; /** @@ -198,4 +203,71 @@ public class EntityFieldConfig implements VerifyLogAware { return die("displayValueFor("+answer+"): unsupported control type: "+getControl()); } } + + public Object example() { + final DateTime today = new DateTime(); + switch (getTypeOrDefault()) { + case flag: return true; + case date_future: return now()+DAYS.toMillis(1); + case date_past: return now()-DAYS.toMillis(1); + case epoch_time: case date: return now(); + case age: return ""+42; + case currency: return "USD"; + case decimal: return 10.5; + case email: return "someone@example.com"; + case embedded: return instantiate(this.objectType); + case error: return "An error occurred"; + case expiration_time: case time_duration: + return MINUTES.toMillis(1); + case hostname: case fqdn: return "test.example.com"; + case http_url: return "https://example.com/"; + case integer: return 42; + case ip4: return "10.0.1.42"; + case ip6: return "fd00::42"; + case json: return "{}"; + case json_array: return "[]"; + case locale: return "en_US"; + case money_decimal: return "10.42"; + case money_integer: return "1042"; + case us_state: return "ND"; + case us_zip: return "90210"; + case us_phone: return "+18885551212"; + case time_zone: return "America/New York"; + case year: return ""+ today.year().get(); + case year_future: return ""+(today.year().get()+1); + case year_past: return ""+(today.year().get()-1); + case year_and_month: return ""+ today.year().get() + "-"+today.monthOfYear().get(); + case year_and_month_future: return ""+ (today.year().get()+1) + "-"+today.monthOfYear().get(); + case year_and_month_past: return ""+ (today.year().get()-1) + "-"+today.monthOfYear().get(); + case string: + default: return "foo"; + } + } + + public Schema openApiType() { + return openApiBaseType().name(getName()) + .title(getDisplayName()) + .example(example()) + .readOnly(readOnly()); + } + + private Schema openApiBaseType() { + switch (getTypeOrDefault()) { + case flag: return new Schema().type("boolean"); + + case date_future: case date_past: case date: + case epoch_time: case expiration_time: case time_duration: + case money_integer: return new Schema().type("integer").format("int64"); + + case age: case integer: case year: case year_future: case year_past: + case year_and_month: case year_and_month_future: case year_and_month_past: + return new Schema().type("integer").format("int32"); + + case decimal: return new Schema().type("number").format("double"); + + case embedded: return null; // todo: better handling of embedded types + + default: return new Schema().type("string"); + } + } } diff --git a/wizard-common/src/main/java/org/cobbzilla/wizard/model/entityconfig/EntityFieldType.java b/wizard-common/src/main/java/org/cobbzilla/wizard/model/entityconfig/EntityFieldType.java index b693912..369a69c 100644 --- a/wizard-common/src/main/java/org/cobbzilla/wizard/model/entityconfig/EntityFieldType.java +++ b/wizard-common/src/main/java/org/cobbzilla/wizard/model/entityconfig/EntityFieldType.java @@ -32,6 +32,12 @@ public enum EntityFieldType { /** a string of characters */ string (new EntityConfigFieldValidator_string()), + /** valid JSON object (as a string) */ + json (new EntityConfigFieldValidator_json()), + + /** valid JSON array (as a string) */ + json_array (new EntityConfigFieldValidator_json_array()), + /** a string of characters where comparisons like lt/le/gt/ge are not useful */ opaque_string (new EntityConfigFieldValidator_string()), diff --git a/wizard-common/src/main/java/org/cobbzilla/wizard/model/entityconfig/EntityReferences.java b/wizard-common/src/main/java/org/cobbzilla/wizard/model/entityconfig/EntityReferences.java index 758042c..0ee6c13 100644 --- a/wizard-common/src/main/java/org/cobbzilla/wizard/model/entityconfig/EntityReferences.java +++ b/wizard-common/src/main/java/org/cobbzilla/wizard/model/entityconfig/EntityReferences.java @@ -23,6 +23,7 @@ import static org.cobbzilla.util.collection.ArrayUtil.arrayToString; import static org.cobbzilla.util.daemon.ZillaRuntime.die; import static org.cobbzilla.util.daemon.ZillaRuntime.empty; import static org.cobbzilla.util.string.StringUtil.camelCaseToSnakeCase; +import static org.cobbzilla.wizard.model.entityconfig.EntityConfig.ENTITY_FILTER; @NoArgsConstructor @Accessors(chain=true) @Slf4j public class EntityReferences { @@ -47,7 +48,7 @@ public class EntityReferences { // find entity classes final List> classes = new ClasspathScanner() .setPackages(packages) - .setFilter(EntityConfig.ENTITY_FILTER) + .setFilter(ENTITY_FILTER) .scan(); final Topology> topology = new Topology<>(); classes.forEach(c -> topology.addNode(c, getReferencedEntities(c))); @@ -57,7 +58,7 @@ public class EntityReferences { public static boolean hasForeignKey(Class candidate, Class entityClass) { if (candidate.equals(Object.class)) return false; if (Arrays.stream(candidate.getDeclaredFields()) - .filter(EntityReferences.FIELD_HAS_FK) + .filter(FIELD_HAS_FK) .anyMatch(f -> f.getAnnotation(ECForeignKey.class).entity().equals(entityClass))) { return true; } @@ -96,7 +97,7 @@ public class EntityReferences { final List constraints = new ArrayList<>(); new ClasspathScanner<>() .setPackages(packages) - .setFilter(EntityConfig.ENTITY_FILTER) + .setFilter(ENTITY_FILTER) .scan() .forEach(c -> constraints.addAll(constraintsForClass((Class) c, includeIndexes))); return constraints; @@ -106,8 +107,8 @@ public class EntityReferences { final List> refs = new ArrayList<>(); while (!clazz.getName().equals(Object.class.getName())) { refs.addAll(Arrays.stream(clazz.getDeclaredFields()) - .filter(EntityReferences.FIELD_HAS_CASCADING_FK) - .map(EntityReferences.FIELD_TO_FK_CLASS) + .filter(FIELD_HAS_CASCADING_FK) + .map(FIELD_TO_FK_CLASS) .collect(Collectors.toList())); clazz = clazz.getSuperclass(); } diff --git a/wizard-common/src/main/java/org/cobbzilla/wizard/model/entityconfig/validation/EntityConfigFieldValidator_json.java b/wizard-common/src/main/java/org/cobbzilla/wizard/model/entityconfig/validation/EntityConfigFieldValidator_json.java new file mode 100644 index 0000000..5f3b319 --- /dev/null +++ b/wizard-common/src/main/java/org/cobbzilla/wizard/model/entityconfig/validation/EntityConfigFieldValidator_json.java @@ -0,0 +1,36 @@ +package org.cobbzilla.wizard.model.entityconfig.validation; + +import com.fasterxml.jackson.databind.JsonNode; +import org.cobbzilla.wizard.model.entityconfig.EntityFieldConfig; +import org.cobbzilla.wizard.validation.ValidationResult; +import org.cobbzilla.wizard.validation.Validator; + +import java.util.Locale; + +import static org.cobbzilla.util.daemon.ZillaRuntime.empty; +import static org.cobbzilla.util.daemon.ZillaRuntime.shortError; +import static org.cobbzilla.util.json.JsonUtil.json; + +public class EntityConfigFieldValidator_json extends EntityConfigFieldValidator_string { + + @Override public ValidationResult validate(Locale locale, Validator validator, EntityFieldConfig fieldConfig, + Object value) { + ValidationResult validation = super.validate(locale, validator, fieldConfig, value); + if (validation.isInvalid()) return validation; + final String val = empty(value) ? "" : value.toString().trim(); + if (empty(val)) return fieldConfig.required() ? new ValidationResult("err."+fieldConfig.getName()+".required") : null; + try { + json(value.toString(), getJsonClass()); + } catch (Exception e) { + return new ValidationResult("err."+fieldConfig.getName()+".invalid", "Error converting to JSON: "+shortError(e), ""+value); + } + return validation; + } + + public Class getJsonClass() { return JsonNode.class; } + + @Override public Object toObject(Locale locale, String value) { + return empty(value) ? "" : value.toString().trim(); + } + +} diff --git a/wizard-common/src/main/java/org/cobbzilla/wizard/model/entityconfig/validation/EntityConfigFieldValidator_json_array.java b/wizard-common/src/main/java/org/cobbzilla/wizard/model/entityconfig/validation/EntityConfigFieldValidator_json_array.java new file mode 100644 index 0000000..7da46a6 --- /dev/null +++ b/wizard-common/src/main/java/org/cobbzilla/wizard/model/entityconfig/validation/EntityConfigFieldValidator_json_array.java @@ -0,0 +1,10 @@ +package org.cobbzilla.wizard.model.entityconfig.validation; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; + +public class EntityConfigFieldValidator_json_array extends EntityConfigFieldValidator_json { + + @Override public Class getJsonClass() { return ArrayNode.class; } + +} diff --git a/wizard-server/pom.xml b/wizard-server/pom.xml index ec1634d..7c70847 100644 --- a/wizard-server/pom.xml +++ b/wizard-server/pom.xml @@ -276,24 +276,6 @@ This code is available under the Apache License, version 2: http://www.apache.or opencsv 4.1 - - - - io.swagger.core.v3 - swagger-jaxrs2 - ${swagger.version} - - - io.swagger.core.v3 - swagger-integration - ${swagger.version} - - - io.swagger.core.v3 - swagger-annotations - ${swagger.version} - - diff --git a/wizard-server/src/main/java/org/cobbzilla/wizard/dao/AbstractDAO.java b/wizard-server/src/main/java/org/cobbzilla/wizard/dao/AbstractDAO.java index d255d24..14d159b 100644 --- a/wizard-server/src/main/java/org/cobbzilla/wizard/dao/AbstractDAO.java +++ b/wizard-server/src/main/java/org/cobbzilla/wizard/dao/AbstractDAO.java @@ -12,6 +12,7 @@ import org.cobbzilla.util.collection.NameAndValue; import org.cobbzilla.util.string.StringUtil; import org.cobbzilla.wizard.model.Identifiable; import org.cobbzilla.wizard.model.IdentifiableBase; +import org.cobbzilla.wizard.model.search.SearchResults; import org.cobbzilla.wizard.model.search.SearchQuery; import org.cobbzilla.wizard.model.search.SqlViewField; import org.cobbzilla.wizard.server.config.PgRestServerConfiguration; diff --git a/wizard-server/src/main/java/org/cobbzilla/wizard/dao/AbstractElasticSearchDAO.java b/wizard-server/src/main/java/org/cobbzilla/wizard/dao/AbstractElasticSearchDAO.java index 61b8180..7453267 100644 --- a/wizard-server/src/main/java/org/cobbzilla/wizard/dao/AbstractElasticSearchDAO.java +++ b/wizard-server/src/main/java/org/cobbzilla/wizard/dao/AbstractElasticSearchDAO.java @@ -13,6 +13,7 @@ import org.apache.commons.pool2.impl.GenericObjectPoolConfig; import org.cobbzilla.util.cache.AutoRefreshingReference; import org.cobbzilla.util.http.URIUtil; import org.cobbzilla.wizard.model.Identifiable; +import org.cobbzilla.wizard.model.search.SearchResults; import org.cobbzilla.wizard.server.config.ElasticSearchConfig; import org.elasticsearch.action.admin.indices.create.CreateIndexRequest; import org.elasticsearch.action.admin.indices.exists.indices.IndicesExistsRequest; diff --git a/wizard-server/src/main/java/org/cobbzilla/wizard/dao/AbstractLdapDAO.java b/wizard-server/src/main/java/org/cobbzilla/wizard/dao/AbstractLdapDAO.java index 6f6c516..f215f9e 100644 --- a/wizard-server/src/main/java/org/cobbzilla/wizard/dao/AbstractLdapDAO.java +++ b/wizard-server/src/main/java/org/cobbzilla/wizard/dao/AbstractLdapDAO.java @@ -6,6 +6,7 @@ import org.apache.commons.collections.CollectionUtils; import org.apache.commons.collections.Transformer; import org.cobbzilla.util.collection.NameAndValue; import org.cobbzilla.wizard.ldap.LdapService; +import org.cobbzilla.wizard.model.search.SearchResults; import org.cobbzilla.wizard.model.search.SearchQuery; import org.cobbzilla.wizard.model.ldap.LdapEntity; import org.cobbzilla.wizard.server.config.LdapConfiguration; diff --git a/wizard-server/src/main/java/org/cobbzilla/wizard/dao/AbstractRedisDAO.java b/wizard-server/src/main/java/org/cobbzilla/wizard/dao/AbstractRedisDAO.java index 813b420..304a3dc 100644 --- a/wizard-server/src/main/java/org/cobbzilla/wizard/dao/AbstractRedisDAO.java +++ b/wizard-server/src/main/java/org/cobbzilla/wizard/dao/AbstractRedisDAO.java @@ -3,6 +3,7 @@ package org.cobbzilla.wizard.dao; import lombok.Getter; import org.cobbzilla.wizard.cache.redis.RedisService; import org.cobbzilla.wizard.model.ExpirableBase; +import org.cobbzilla.wizard.model.search.SearchResults; import org.cobbzilla.wizard.model.search.SearchQuery; import org.springframework.beans.factory.annotation.Autowired; diff --git a/wizard-server/src/main/java/org/cobbzilla/wizard/dao/DAO.java b/wizard-server/src/main/java/org/cobbzilla/wizard/dao/DAO.java index 70b82d5..324246d 100644 --- a/wizard-server/src/main/java/org/cobbzilla/wizard/dao/DAO.java +++ b/wizard-server/src/main/java/org/cobbzilla/wizard/dao/DAO.java @@ -2,6 +2,7 @@ package org.cobbzilla.wizard.dao; import lombok.NonNull; import org.cobbzilla.wizard.model.Identifiable; +import org.cobbzilla.wizard.model.search.SearchResults; import org.cobbzilla.wizard.model.search.SearchQuery; import org.hibernate.criterion.Order; diff --git a/wizard-server/src/main/java/org/cobbzilla/wizard/dao/SqlViewSearchHelper.java b/wizard-server/src/main/java/org/cobbzilla/wizard/dao/SqlViewSearchHelper.java index f9430d6..f44bada 100644 --- a/wizard-server/src/main/java/org/cobbzilla/wizard/dao/SqlViewSearchHelper.java +++ b/wizard-server/src/main/java/org/cobbzilla/wizard/dao/SqlViewSearchHelper.java @@ -6,6 +6,7 @@ import org.cobbzilla.util.jdbc.ResultSetBean; import org.cobbzilla.util.reflect.ReflectionUtil; import org.cobbzilla.util.string.StringUtil; import org.cobbzilla.wizard.model.Identifiable; +import org.cobbzilla.wizard.model.search.SearchResults; import org.cobbzilla.wizard.model.search.SearchQuery; import org.cobbzilla.wizard.model.search.SearchSort; import org.cobbzilla.wizard.model.search.SqlViewField; diff --git a/wizard-server/src/main/java/org/cobbzilla/wizard/dao/shard/AbstractShardedDAO.java b/wizard-server/src/main/java/org/cobbzilla/wizard/dao/shard/AbstractShardedDAO.java index 4c1c8d5..24a4c33 100644 --- a/wizard-server/src/main/java/org/cobbzilla/wizard/dao/shard/AbstractShardedDAO.java +++ b/wizard-server/src/main/java/org/cobbzilla/wizard/dao/shard/AbstractShardedDAO.java @@ -8,7 +8,7 @@ import org.cobbzilla.util.reflect.ReflectionUtil; import org.cobbzilla.wizard.cache.redis.HasRedisConfiguration; import org.cobbzilla.wizard.cache.redis.RedisService; import org.cobbzilla.wizard.dao.DAO; -import org.cobbzilla.wizard.dao.SearchResults; +import org.cobbzilla.wizard.model.search.SearchResults; import org.cobbzilla.wizard.dao.shard.cache.ShardCacheableFindByUnique2FieldFinder; import org.cobbzilla.wizard.dao.shard.cache.ShardCacheableFindByUnique3FieldFinder; import org.cobbzilla.wizard.dao.shard.cache.ShardCacheableIdentityFinder; diff --git a/wizard-server/src/main/java/org/cobbzilla/wizard/docstore/mongo/MongoDocStoreDAOBase.java b/wizard-server/src/main/java/org/cobbzilla/wizard/docstore/mongo/MongoDocStoreDAOBase.java index 438c85f..bde2975 100644 --- a/wizard-server/src/main/java/org/cobbzilla/wizard/docstore/mongo/MongoDocStoreDAOBase.java +++ b/wizard-server/src/main/java/org/cobbzilla/wizard/docstore/mongo/MongoDocStoreDAOBase.java @@ -2,7 +2,7 @@ package org.cobbzilla.wizard.docstore.mongo; import lombok.Getter; import org.cobbzilla.wizard.dao.DAO; -import org.cobbzilla.wizard.dao.SearchResults; +import org.cobbzilla.wizard.model.search.SearchResults; import org.cobbzilla.wizard.model.search.SearchQuery; import javax.validation.Valid; diff --git a/wizard-server/src/main/java/org/cobbzilla/wizard/filters/EntityTypeHeaderResponseFilter.java b/wizard-server/src/main/java/org/cobbzilla/wizard/filters/EntityTypeHeaderResponseFilter.java index be19452..f771195 100644 --- a/wizard-server/src/main/java/org/cobbzilla/wizard/filters/EntityTypeHeaderResponseFilter.java +++ b/wizard-server/src/main/java/org/cobbzilla/wizard/filters/EntityTypeHeaderResponseFilter.java @@ -1,7 +1,7 @@ package org.cobbzilla.wizard.filters; import lombok.extern.slf4j.Slf4j; -import org.cobbzilla.wizard.dao.SearchResults; +import org.cobbzilla.wizard.model.search.SearchResults; import org.cobbzilla.wizard.model.Identifiable; import javax.ws.rs.container.ContainerRequestContext; diff --git a/wizard-server/src/main/java/org/cobbzilla/wizard/dao/SearchResults.java b/wizard-server/src/main/java/org/cobbzilla/wizard/model/search/SearchResults.java similarity index 81% rename from wizard-server/src/main/java/org/cobbzilla/wizard/dao/SearchResults.java rename to wizard-server/src/main/java/org/cobbzilla/wizard/model/search/SearchResults.java index 21d62cb..11c7a37 100644 --- a/wizard-server/src/main/java/org/cobbzilla/wizard/dao/SearchResults.java +++ b/wizard-server/src/main/java/org/cobbzilla/wizard/model/search/SearchResults.java @@ -1,7 +1,8 @@ -package org.cobbzilla.wizard.dao; +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; @@ -9,7 +10,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.search.SearchQuery; +import org.cobbzilla.wizard.model.entityconfig.annotations.ECField; import java.util.ArrayList; import java.util.List; @@ -19,7 +20,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) +@NoArgsConstructor @Accessors(chain=true) @Schema public class SearchResults implements Scrubbable { public static final ScrubbableField[] SCRUBBABLE_FIELDS = new ScrubbableField[]{ @@ -33,7 +34,7 @@ public class SearchResults implements Scrubbable { @Override public ScrubbableField[] fieldsToScrub() { return SCRUBBABLE_FIELDS; } - private static Map jsonTypeCache = new ConcurrentHashMap<>(); + private static final Map jsonTypeCache = new ConcurrentHashMap<>(); public static JavaType jsonType(Class klazz) { JavaType type = jsonTypeCache.get(klazz); if (type == null) { @@ -43,10 +44,10 @@ public class SearchResults implements Scrubbable { return type; } - @Getter @Setter private List results = new ArrayList<>(); - @Getter @Setter private Integer totalCount; - @Getter @Setter private String nextPage; - @Getter @Setter private String error; + @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; 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 4f11a33..a52e5fe 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 @@ -1,5 +1,11 @@ package org.cobbzilla.wizard.resources; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.ExampleObject; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; @@ -32,12 +38,18 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; import static javax.ws.rs.core.MediaType.APPLICATION_JSON; +import static org.cobbzilla.util.daemon.ZillaRuntime.empty; +import static org.cobbzilla.util.daemon.ZillaRuntime.shortError; +import static org.cobbzilla.util.http.HttpStatusCodes.SC_NOT_FOUND; +import static org.cobbzilla.util.http.HttpStatusCodes.SC_OK; import static org.cobbzilla.util.io.StreamUtil.loadResourceAsStream; import static org.cobbzilla.util.json.JsonUtil.FULL_MAPPER_ALLOW_COMMENTS; import static org.cobbzilla.util.json.JsonUtil.fromJson; import static org.cobbzilla.util.reflect.ReflectionUtil.forName; import static org.cobbzilla.util.string.StringUtil.packagePath; import static org.cobbzilla.wizard.resources.ResourceUtil.*; +import static org.cobbzilla.wizard.server.config.OpenApiConfiguration.API_TAG_UTILITY; +import static org.cobbzilla.wizard.server.config.OpenApiConfiguration.SEC_API_KEY; @Consumes(APPLICATION_JSON) @Produces(APPLICATION_JSON) @@ -62,17 +74,35 @@ public abstract class AbstractEntityConfigsResource implements EntityConfigSourc } @GET + @Operation(security=@SecurityRequirement(name=SEC_API_KEY), + 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={ + @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) { if (!authorized(ctx)) return forbidden(); return ok(full != null && full ? getConfigs().getEntries() : getConfigs().get().keySet()); } + @Override public EntityConfig getOrCreateEntityConfig(Object thing) throws Exception { + final EntityConfig entityConfig = getEntityConfig(thing); + return entityConfig != null ? entityConfig : getEntityConfig(toClass(thing), false); + } + @Override public EntityConfig getEntityConfig(Object thing) { final AutoRefreshingReference> configs = getConfigs(); final Map configMap = configs.get(); synchronized (configMap) { - Class clazz = thing instanceof Class ? (Class) thing : thing.getClass(); + Class clazz = toClass(thing); do { final EntityConfig entityConfig = configMap.get(clazz.getName().toLowerCase()); if (entityConfig != null) return entityConfig; @@ -82,7 +112,21 @@ public abstract class AbstractEntityConfigsResource implements EntityConfigSourc return null; } + public Class toClass(Object thing) { + return thing instanceof Class ? (Class) thing : thing.getClass(); + } + @GET @Path("/{name}") + @Operation(security=@SecurityRequirement(name=SEC_API_KEY), + 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.")}, + 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") + } + ) public Response getConfig (@Context ContainerRequest ctx, @PathParam("name") String name, @QueryParam("debug") boolean debug, @@ -181,14 +225,21 @@ public abstract class AbstractEntityConfigsResource implements EntityConfigSourc entityConfig.setClassName(clazz.getName()); try { - final DAO dao = getConfiguration().getDaoForEntityClass(clazz); final EntityConfig updated = entityConfig.updateWithAnnotations(clazz, root); + DAO dao = null; + try { + dao = getConfiguration().getDaoForEntityClass(clazz); + } catch (Exception e) { + log.info("getEntityConfig: creating EntityConfig for class without a DAO: "+clazz.getName()+": "+shortError(e)); + } + // add SQL search fields, if the entity supports them if (SqlViewSearchResult.class.isAssignableFrom(clazz) && dao instanceof AbstractCRUDDAO) { updated.setSqlViewFields(((AbstractCRUDDAO) dao).getSearchFields()); } + if (empty(updated.getName())) updated.setName(clazz.getSimpleName()); return updated; } catch (Exception e) { 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 a5853ab..34e1dc7 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,6 +2,7 @@ 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; @@ -17,13 +18,15 @@ 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.entityconfig.EntityConfig; +import org.cobbzilla.wizard.model.entityconfig.EntityConfigSource; +import org.cobbzilla.wizard.util.ClasspathScanner; import org.glassfish.jersey.server.ResourceConfig; +import org.springframework.core.type.filter.AnnotationTypeFilter; import java.util.*; -import java.util.stream.Collectors; -import static org.cobbzilla.util.daemon.ZillaRuntime.die; -import static org.cobbzilla.util.daemon.ZillaRuntime.empty; +import static org.cobbzilla.util.daemon.ZillaRuntime.*; @Slf4j public class OpenApiConfiguration { @@ -31,6 +34,7 @@ public class OpenApiConfiguration { // set contactEmail to this value to disable OpenAPI public static final String OPENAPI_DISABLED = "openapi_disabled"; public static final String SEC_API_KEY = "apiKey"; + public static final String API_TAG_UTILITY = "utility"; @Getter @Setter private String title; @Getter @Setter private String description; @@ -38,6 +42,7 @@ public class OpenApiConfiguration { @Getter @Setter private String terms; @Getter @Setter private String licenseName; @Getter @Setter private String licenseUrl; + @Getter @Setter private String[] additionalPackages; public boolean valid() { return !empty(contactEmail) && !contactEmail.equalsIgnoreCase(OPENAPI_DISABLED) @@ -89,14 +94,56 @@ public class OpenApiConfiguration { .info(info) .servers(servers); + final Set packages = getPackages(configuration); + if (configuration instanceof HasDatabaseConfiguration) { + try { + addEntitySchemas(oas, packages.toArray(String[]::new), configuration); + } catch (Exception e) { + log.warn("register: error reading entity configs or converting to OpenApi schemas: "+shortError(e)); + } + } + final SwaggerConfiguration oasConfig = new SwaggerConfiguration() .openAPI(oas) .prettyPrint(true) - .resourcePackages(Arrays.stream(configuration.getJersey().getResourcePackages()).collect(Collectors.toSet())); + .resourcePackages(packages); rc.register(new OpenApiResource().openApiConfiguration(oasConfig)); } + public static final AnnotationTypeFilter SCHEMA_FILTER = new AnnotationTypeFilter(Schema.class); + + protected void addEntitySchemas(OpenAPI oas, String[] packages, RestServerConfiguration configuration) throws Exception { + final EntityConfigSource entityConfigSource = configuration.getBean(EntityConfigSource.class); + final Set> apiEntities = new HashSet<>(); + final PgRestServerConfiguration pgConfig = (PgRestServerConfiguration) configuration; + apiEntities.addAll(pgConfig.getEntityClassesReverse()); + apiEntities.addAll(new ClasspathScanner<>() + .setPackages(packages) + .setFilter(SCHEMA_FILTER) + .scan()); + for (Class entity : apiEntities) { + final EntityConfig entityConfig = entityConfigSource.getOrCreateEntityConfig(entity); + oas.schema(entityConfig.example().getClass().getSimpleName(), entityConfig.openApiSchema()); + } + } + + protected Set getPackages(RestServerConfiguration configuration) { + // always add jersey resources + final Set packages + = new HashSet<>(Arrays.asList(configuration.getJersey().getResourcePackages())); + + // add entities if we have them + if (configuration instanceof HasDatabaseConfiguration) { + final DatabaseConfiguration db = ((HasDatabaseConfiguration) configuration).getDatabase(); + packages.addAll(Arrays.asList(db.getHibernate().getEntityPackages())); + } + if (!empty(additionalPackages)) { + packages.addAll(Arrays.asList(additionalPackages)); + } + return packages; + } + public String subst (String value, Handlebars handlebars, Map ctx, diff --git a/wizard-server/src/main/java/org/cobbzilla/wizard/server/listener/BrowserLauncherListener.java b/wizard-server/src/main/java/org/cobbzilla/wizard/server/listener/BrowserLauncherListener.java index 1ac045a..f41ee20 100644 --- a/wizard-server/src/main/java/org/cobbzilla/wizard/server/listener/BrowserLauncherListener.java +++ b/wizard-server/src/main/java/org/cobbzilla/wizard/server/listener/BrowserLauncherListener.java @@ -27,8 +27,12 @@ public class BrowserLauncherListener extends RestServerLifecycleListenerBase { final Thread appThread = new Thread(() -> { final boolean allowLaunch = configAllowsBrowserLaunch(config); final Desktop desktop = allowLaunch && isDesktopSupported() ? Desktop.getDesktop() : null; + final String versionInfo = server.getConfiguration().hasVersion() + ? "\nVersion: " + server.getConfiguration().getVersion() + : ""; if (desktop != null && desktop.isSupported(Desktop.Action.BROWSE)) { try { + log.info("\n\n"+server.getConfiguration().getServerName()+" Successfully Started"+versionInfo+"\n\nLaunching browser to: "+baseUri+"\n\nSet env var "+getDisableBrowserAutoLaunchEnvVarName()+"=true to disable browser launching\n\nHit Control-C to stop the server\n"); desktop.browse(URIUtil.toUri(baseUri)); } catch (Exception e) { final String msg = "onStart: error launching default browser with url '" + baseUri + "': " + e; @@ -37,9 +41,6 @@ public class BrowserLauncherListener extends RestServerLifecycleListenerBase { } } else { // no browser. tell the user where the server is listening via log statement - final String versionInfo = server.getConfiguration().hasVersion() - ? "\nVersion: " + server.getConfiguration().getVersion() - : ""; log.info("\n\n"+server.getConfiguration().getServerName()+" Successfully Started"+versionInfo+"\n\nNot launching browser: System lacks a browser and/or desktop window manager.\n\nWeb UI is: "+baseUri+"\nAPI is: "+baseUri+"/api\nHit Control-C to stop the server\n"); } });