@@ -98,6 +98,23 @@ This code is available under the Apache License, version 2: http://www.apache.or | |||
<version>2.3.0</version> | |||
</dependency> | |||
<!-- API docs --> | |||
<dependency> | |||
<groupId>io.swagger.core.v3</groupId> | |||
<artifactId>swagger-jaxrs2</artifactId> | |||
<version>${swagger.version}</version> | |||
</dependency> | |||
<dependency> | |||
<groupId>io.swagger.core.v3</groupId> | |||
<artifactId>swagger-integration</artifactId> | |||
<version>${swagger.version}</version> | |||
</dependency> | |||
<dependency> | |||
<groupId>io.swagger.core.v3</groupId> | |||
<artifactId>swagger-annotations</artifactId> | |||
<version>${swagger.version}</version> | |||
</dependency> | |||
</dependencies> | |||
</project> |
@@ -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> T example() { return (T) getExample(); } | |||
public <T> Schema<T> openApiSchema() { | |||
final T defaultObj = instantiate(className); | |||
final T example = example(); | |||
final Schema<T> 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<String, Schema> props = new HashMap<>(); | |||
final List<String> required = new ArrayList<>(); | |||
for (String field : fieldNames) { | |||
final Schema<Object> fieldSchema = fieldToOpenApiSchema(this.getFields().get(field), required); | |||
props.put(field, fieldSchema); | |||
} | |||
s.required(required).properties(props); | |||
return s; | |||
} | |||
private <T> Schema<T> fieldToOpenApiSchema(EntityFieldConfig fieldConfig, List<String> required) { | |||
if (fieldConfig.required()) required.add(fieldConfig.getDisplayName()); | |||
return fieldConfig.openApiType(); | |||
} | |||
} |
@@ -3,5 +3,5 @@ package org.cobbzilla.wizard.model.entityconfig; | |||
public interface EntityConfigSource { | |||
EntityConfig getEntityConfig(Object thing); | |||
EntityConfig getOrCreateEntityConfig(Object thing) throws Exception; | |||
} |
@@ -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<EntityFieldConfig> { | |||
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<Boolean>().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<Long>().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<Integer>().type("integer").format("int32"); | |||
case decimal: return new Schema<Double>().type("number").format("double"); | |||
case embedded: return null; // todo: better handling of embedded types | |||
default: return new Schema<String>().type("string"); | |||
} | |||
} | |||
} |
@@ -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()), | |||
@@ -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<Class<? extends Identifiable>> classes = new ClasspathScanner<Identifiable>() | |||
.setPackages(packages) | |||
.setFilter(EntityConfig.ENTITY_FILTER) | |||
.setFilter(ENTITY_FILTER) | |||
.scan(); | |||
final Topology<Class<? extends Identifiable>> 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<? extends Identifiable> 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<String> constraints = new ArrayList<>(); | |||
new ClasspathScanner<>() | |||
.setPackages(packages) | |||
.setFilter(EntityConfig.ENTITY_FILTER) | |||
.setFilter(ENTITY_FILTER) | |||
.scan() | |||
.forEach(c -> constraints.addAll(constraintsForClass((Class<? extends Identifiable>) c, includeIndexes))); | |||
return constraints; | |||
@@ -106,8 +107,8 @@ public class EntityReferences { | |||
final List<Class<? extends Identifiable>> 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(); | |||
} | |||
@@ -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<? extends JsonNode> getJsonClass() { return JsonNode.class; } | |||
@Override public Object toObject(Locale locale, String value) { | |||
return empty(value) ? "" : value.toString().trim(); | |||
} | |||
} |
@@ -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<? extends JsonNode> getJsonClass() { return ArrayNode.class; } | |||
} |
@@ -276,24 +276,6 @@ This code is available under the Apache License, version 2: http://www.apache.or | |||
<artifactId>opencsv</artifactId> | |||
<version>4.1</version> | |||
</dependency> | |||
<!-- API docs --> | |||
<dependency> | |||
<groupId>io.swagger.core.v3</groupId> | |||
<artifactId>swagger-jaxrs2</artifactId> | |||
<version>${swagger.version}</version> | |||
</dependency> | |||
<dependency> | |||
<groupId>io.swagger.core.v3</groupId> | |||
<artifactId>swagger-integration</artifactId> | |||
<version>${swagger.version}</version> | |||
</dependency> | |||
<dependency> | |||
<groupId>io.swagger.core.v3</groupId> | |||
<artifactId>swagger-annotations</artifactId> | |||
<version>${swagger.version}</version> | |||
</dependency> | |||
</dependencies> | |||
</project> |
@@ -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; | |||
@@ -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; | |||
@@ -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; | |||
@@ -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; | |||
@@ -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; | |||
@@ -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; | |||
@@ -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; | |||
@@ -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; | |||
@@ -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; | |||
@@ -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<E> implements Scrubbable { | |||
public static final ScrubbableField[] SCRUBBABLE_FIELDS = new ScrubbableField[]{ | |||
@@ -33,7 +34,7 @@ public class SearchResults<E> implements Scrubbable { | |||
@Override public ScrubbableField[] fieldsToScrub() { return SCRUBBABLE_FIELDS; } | |||
private static Map<Class, JavaType> jsonTypeCache = new ConcurrentHashMap<>(); | |||
private static final Map<Class, JavaType> jsonTypeCache = new ConcurrentHashMap<>(); | |||
public static JavaType jsonType(Class klazz) { | |||
JavaType type = jsonTypeCache.get(klazz); | |||
if (type == null) { | |||
@@ -43,10 +44,10 @@ public class SearchResults<E> implements Scrubbable { | |||
return type; | |||
} | |||
@Getter @Setter private List<E> results = new ArrayList<>(); | |||
@Getter @Setter private Integer totalCount; | |||
@Getter @Setter private String nextPage; | |||
@Getter @Setter private String error; | |||
@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; | |||
public String getResultType() { return empty(results) ? null : results.get(0).getClass().getName(); } | |||
public void setResultType (String val) {} // noop |
@@ -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<Map<String, EntityConfig>> configs = getConfigs(); | |||
final Map<String, EntityConfig> 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) { | |||
@@ -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<String> 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<Class<?>> 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<String> getPackages(RestServerConfiguration configuration) { | |||
// always add jersey resources | |||
final Set<String> 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<String, Object> ctx, | |||
@@ -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"); | |||
} | |||
}); | |||