package io.github.ibuildthecloud.gdapi.factory.impl;
import io.cattle.platform.util.type.CollectionUtils;
import io.github.ibuildthecloud.gdapi.annotation.Actions;
import io.github.ibuildthecloud.gdapi.factory.SchemaFactory;
import io.github.ibuildthecloud.gdapi.model.Action;
import io.github.ibuildthecloud.gdapi.model.ApiError;
import io.github.ibuildthecloud.gdapi.model.ApiVersion;
import io.github.ibuildthecloud.gdapi.model.Collection;
import io.github.ibuildthecloud.gdapi.model.Field;
import io.github.ibuildthecloud.gdapi.model.FieldType;
import io.github.ibuildthecloud.gdapi.model.FieldType.TypeAndName;
import io.github.ibuildthecloud.gdapi.model.Resource;
import io.github.ibuildthecloud.gdapi.model.Schema;
import io.github.ibuildthecloud.gdapi.model.impl.FieldImpl;
import io.github.ibuildthecloud.gdapi.model.impl.SchemaImpl;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import javax.annotation.PostConstruct;
import javax.inject.Inject;
import org.apache.commons.beanutils.PropertyUtils;
import org.apache.commons.lang3.StringUtils;
@io.github.ibuildthecloud.gdapi.annotation.Type
public class SchemaFactoryImpl extends AbstractSchemaFactory implements SchemaFactory {
final io.github.ibuildthecloud.gdapi.annotation.Field defaultField;
final io.github.ibuildthecloud.gdapi.annotation.Type defaultType;
String id = "base";
boolean includeDefaultTypes = true, writableByDefault = false;
Map<String, SchemaImpl> schemasByName = new TreeMap<String, SchemaImpl>();
Map<Class<?>, SchemaImpl> schemasByClass = new HashMap<Class<?>, SchemaImpl>();
Map<String, Class<?>> typeToClass = new HashMap<String, Class<?>>();
List<Class<?>> types = new ArrayList<Class<?>>();
List<String> typeNames = new ArrayList<String>();
Map<SchemaImpl, Class<?>> parentClasses = new HashMap<SchemaImpl, Class<?>>();
List<Schema> schemasList = new ArrayList<Schema>();
List<SchemaPostProcessor> postProcessors = new ArrayList<SchemaPostProcessor>();
public SchemaFactoryImpl() {
try {
defaultField =
PropertyUtils.getPropertyDescriptor(this, "defaultField").getReadMethod()
.getAnnotation(io.github.ibuildthecloud.gdapi.annotation.Field.class);
defaultType = this.getClass().getAnnotation(io.github.ibuildthecloud.gdapi.annotation.Type.class);
} catch (Exception e) {
throw new IllegalStateException(e);
}
}
@Override
public String getId() {
return id;
}
@io.github.ibuildthecloud.gdapi.annotation.Field
public Object getDefaultField() {
return null;
}
@Override
public Schema registerSchema(Object obj) {
Class<?> clz = obj instanceof Class<?> ? (Class<?>)obj : null;
SchemaImpl schema = schemaFromObject(obj);
for (SchemaPostProcessor processor : postProcessors) {
schema = processor.postProcessRegister(schema, this);
if (schema == null) {
return null;
}
}
/* Register in the multitude of maps */
if (clz != null) {
addToMap(typeToClass, schema, clz);
}
addToMap(schemasByName, schema, schema);
if (clz != null) {
schemasByClass.put(clz, schema);
for (Class<?> iface : clz.getInterfaces()) {
schemasByClass.put(iface, schema);
}
}
schemasList.add(schema);
return schema;
}
protected <T> void addToMap(Map<String, T> map, SchemaImpl key, T value) {
if (key == null || value == null)
return;
map.put(key.getId(), value);
map.put(key.getId().toLowerCase(), value);
map.put(key.getPluralName(), value);
map.put(key.getPluralName().toLowerCase(), value);
}
@Override
public Schema getSchema(Class<?> clz) {
return schemasByClass.get(clz);
}
@Override
public Schema parseSchema(String name) {
SchemaImpl schema = readSchema(name);
Class<?> clz = typeToClass.get(name);
processParent(schema);
List<Field> fields = getFields(clz);
for (Map.Entry<String, Field> entry : schema.getResourceFields().entrySet()) {
Field field = entry.getValue();
if (field instanceof FieldImpl) {
((FieldImpl)field).setName(entry.getKey());
}
fields.add(field);
}
Map<String, Field> resourceFields = sortFields(fields);
schema.setResourceFields(resourceFields);
schema.getResourceActions().putAll(getResourceActions(clz));
schema.getCollectionActions().putAll(getCollectionActions(clz));
for (SchemaPostProcessor processor : postProcessors) {
schema = processor.postProcess(schema, this);
}
addToMap(schemasByName, schema, schema);
if (clz == null && schema.getParent() != null) {
clz = typeToClass.get(schema.getParent());
if (clz != null) {
addToMap(typeToClass, schema, clz);
}
}
return schema;
}
protected void processParent(SchemaImpl schema) {
SchemaImpl parent = null;
Class<?> parentClass = parentClasses.get(schema);
String parentName = schema.getParent();
if (parentClass == null && parentName != null) {
parent = schemasByName.get(parentName);
if (parent == null) {
throw new IllegalArgumentException("Failed to find parent schema for [" + parentName + "] for type [" + schema.getId() + "]");
}
} else if (parentClass != null) {
parent = schemasByClass.get(parentClass);
if (parent == null) {
throw new IllegalArgumentException("Failed to find parent schema for class [" + parentClass + "] for type [" + schema.getId() + "]");
}
}
if (parent != null) {
schema.setParent(parent.getId());
parent.getChildren().add(schema.getId());
schema.load(parent);
}
}
protected Map<String, Action> getCollectionActions(Class<?> clz) {
return getActions(clz, true);
}
protected Map<String, Action> getResourceActions(Class<?> clz) {
return getActions(clz, false);
}
protected Map<String, Action> getActions(Class<?> clz, boolean collection) {
Map<String, Action> result = new LinkedHashMap<String, Action>();
if (clz == null) {
return result;
}
Actions actions = clz.getAnnotation(Actions.class);
if (actions == null) {
return result;
}
for (io.github.ibuildthecloud.gdapi.annotation.Action action : actions.value()) {
if (action.collection() != collection) {
continue;
}
String input = null;
String output = null;
if (StringUtils.isBlank(action.inputType())) {
input = getSchemaName(action.input());
} else {
input = action.inputType();
}
if (StringUtils.isBlank(action.outputType())) {
output = getSchemaName(action.output());
} else {
output = action.outputType();
}
result.put(action.name(), new Action(input, output));
}
return result;
}
protected SchemaImpl schemaFromObject(Object obj) {
Class<?> clz = obj instanceof Class<?> ? (Class<?>)obj : obj.getClass();
SchemaImpl schema = new SchemaImpl();
SchemaType schemaType = obj instanceof String ? schemaTypeFromString((String)obj) : schemaTypeFromClass(clz);
schema.setName(schemaType.name);
schema.setPluralName(schemaType.pluralName);
schema.setParent(schemaType.parent);
if (schemaType.parent == null && schemaType.parentClass != null) {
parentClasses.put(schema, schemaType.parentClass);
}
return schema;
}
protected SchemaType schemaTypeFromClass(Class<?> clz) {
SchemaType schemaType = new SchemaType();
io.github.ibuildthecloud.gdapi.annotation.Type type = clz.getAnnotation(io.github.ibuildthecloud.gdapi.annotation.Type.class);
if (type == null)
type = defaultType;
if (!StringUtils.isEmpty(type.name())) {
schemaType.name = type.name();
} else {
schemaType.name = StringUtils.uncapitalize(clz.getSimpleName());
}
if (!StringUtils.isBlank(type.pluralName())) {
schemaType.pluralName = type.pluralName();
}
if (!StringUtils.isBlank(type.parent())) {
schemaType.parent = type.parent();
}
if (type.parentClass() != Void.class) {
schemaType.parentClass = type.parentClass();
}
return schemaType;
}
protected SchemaType schemaTypeFromString(String type) {
SchemaType schemaType = new SchemaType();
String[] parts = type.split("\\s*,\\s*");
schemaType.name = parts[0];
for (int i = 1; i < parts.length; i++) {
String[] kv = parts[i].split("\\s*=\\s*");
if (kv.length != 2) {
throw new IllegalArgumentException("Illegal type format [" + type + "] must be comma separated key=value pairs");
}
String key = kv[0];
String value = kv[1];
if ("pluralName".equals(key)) {
schemaType.pluralName = value;
} else if ("parent".equals(key)) {
schemaType.parent = value;
}
}
return schemaType;
}
protected SchemaImpl readSchema(String name) {
Class<?> clz = typeToClass.get(name);
if (clz == null)
clz = Object.class;
SchemaImpl schema = schemasByName.get(name);
if (schema == null)
schema = schemaFromObject(clz);
io.github.ibuildthecloud.gdapi.annotation.Type type = clz.getAnnotation(io.github.ibuildthecloud.gdapi.annotation.Type.class);
if (type == null)
type = defaultType;
if (type == defaultType) {
schema.setCreate(writableByDefault);
schema.setUpdate(writableByDefault);
schema.setDeletable(writableByDefault);
} else {
schema.setCreate(type.create());
schema.setUpdate(type.update());
schema.setDeletable(type.delete());
}
schema.setById(type.byId());
schema.setList(type.list());
return schema;
}
protected Map<String, Field> sortFields(List<Field> fields) {
Map<Integer, Field> indexed = new TreeMap<Integer, Field>();
Map<String, Field> named = new TreeMap<String, Field>();
Map<String, Field> result = new LinkedHashMap<String, Field>();
for (Field field : fields) {
Integer displayIndex = field.getDisplayIndex();
if (displayIndex == null) {
named.put(field.getName(), field);
} else {
indexed.put(displayIndex, field);
}
}
for (Field field : indexed.values()) {
result.put(field.getName(), field);
}
for (Field field : named.values()) {
result.put(field.getName(), field);
}
return result;
}
protected List<Field> getFields(Class<?> clz) {
List<Field> result = new ArrayList<Field>();
if (clz == null)
return result;
for (PropertyDescriptor prop : PropertyUtils.getPropertyDescriptors(clz)) {
FieldImpl field = getField(clz, prop);
if (field != null) {
result.add(field);
}
}
return result;
}
protected FieldImpl getField(Class<?> clz, PropertyDescriptor prop) {
FieldImpl field = new FieldImpl();
Method readMethod = prop.getReadMethod();
Method writeMethod = prop.getWriteMethod();
if (readMethod == null && writeMethod == null)
return null;
io.github.ibuildthecloud.gdapi.annotation.Field f = getFieldAnnotation(prop);
if (!f.include())
return null;
field.setReadMethod(readMethod);
if (readMethod != null && readMethod.getDeclaringClass() != clz)
return null;
if (StringUtils.isEmpty(f.name())) {
field.setName(prop.getName());
} else {
field.setName(f.name());
}
if (StringUtils.isNotEmpty(f.description())) {
field.setDescription(f.description());
}
if (f.displayIndex() > 0) {
field.setDisplayIndex(f.displayIndex());
}
if (readMethod == null) {
field.setIncludeInList(false);
}
assignSimpleProps(field, f);
assignType(prop, field, f);
assignLengths(field, f);
assignOptions(prop, field, f);
return field;
}
protected void assignOptions(PropertyDescriptor prop, FieldImpl field, io.github.ibuildthecloud.gdapi.annotation.Field f) {
Class<?> clz = prop.getPropertyType();
if (!clz.isEnum()) {
return;
}
List<String> options = new ArrayList<String>(clz.getEnumConstants().length);
for (Object o : clz.getEnumConstants()) {
options.add(o.toString());
}
field.setOptions(options);
}
protected void assignSimpleProps(FieldImpl field, io.github.ibuildthecloud.gdapi.annotation.Field f) {
if (!StringUtils.isEmpty(f.defaultValue())) {
field.setDefault(f.defaultValue());
}
if (!StringUtils.isEmpty(f.validChars())) {
field.setValidChars(f.validChars());
}
if (!StringUtils.isEmpty(f.invalidChars())) {
field.setInvalidChars(f.invalidChars());
}
if (f == this.defaultField) {
field.setNullable(writableByDefault);
field.setUpdate(writableByDefault);
field.setCreate(writableByDefault);
} else {
field.setNullable(f.nullable());
field.setUpdate(f.update());
field.setCreate(f.create());
field.setTransform(f.transform());
}
field.setUnique(f.unique());
field.setRequired(f.required());
}
protected void assignLengths(FieldImpl field, io.github.ibuildthecloud.gdapi.annotation.Field f) {
if (f.min() != Long.MIN_VALUE) {
field.setMin(f.min());
}
if (f.max() != Long.MAX_VALUE) {
field.setMax(f.max());
}
if (f.minLength() != Long.MIN_VALUE) {
field.setMinLength(f.minLength());
}
if (f.maxLength() != Long.MAX_VALUE) {
field.setMaxLength(f.maxLength());
}
}
protected void assignType(PropertyDescriptor prop, FieldImpl field, io.github.ibuildthecloud.gdapi.annotation.Field f) {
if (f.type() != FieldType.NONE) {
field.setTypeEnum(f.type());
return;
}
if (!StringUtils.isEmpty(f.typeString())) {
field.setType(f.typeString());
return;
}
if (f.password()) {
field.setTypeEnum(FieldType.PASSWORD);
return;
}
assignSimpleType(prop.getPropertyType(), field);
List<TypeAndName> types = new ArrayList<FieldType.TypeAndName>();
Method readMethod = prop.getReadMethod();
if (readMethod != null) {
getTypes(readMethod.getGenericReturnType(), types);
}
if (types.size() == 1) {
field.setType(types.get(0).getName());
} else if (types.size() > 1) {
types.remove(0);
field.setSubTypesList(types);
}
}
protected void getTypes(java.lang.reflect.Type type, List<TypeAndName> types) {
Class<?> clz = null;
if (type instanceof Class<?>) {
clz = (Class<?>)type;
}
if (type instanceof ParameterizedType) {
java.lang.reflect.Type rawType = ((ParameterizedType)type).getRawType();
if (rawType instanceof Class<?>)
clz = (Class<?>)rawType;
}
if (clz == null) {
throw new IllegalArgumentException("Failed to find class for type [" + type + "]");
}
FieldType fieldType = assignSimpleType(clz, null);
String name = fieldType.getExternalType();
if (fieldType == FieldType.TYPE) {
Schema subSchema = getSchema(clz);
if (subSchema == null) {
fieldType = FieldType.JSON;
name = fieldType.getExternalType();
} else {
name = subSchema.getId();
}
}
types.add(new TypeAndName(fieldType, name));
java.lang.reflect.Type subType = null;
switch (fieldType) {
case ARRAY:
if (clz.isArray()) {
subType = clz.getComponentType();
} else {
subType = getGenericType(type, 0);
}
break;
case MAP:
subType = getGenericType(type, 1);
break;
case REFERENCE:
subType = getGenericType(type, 0);
break;
case TYPE:
return;
default:
break;
}
if (subType != null) {
getTypes(subType, types);
}
}
protected java.lang.reflect.Type getGenericType(java.lang.reflect.Type t, int index) {
if (t instanceof ParameterizedType && ((ParameterizedType)t).getActualTypeArguments().length == index + 1) {
return ((ParameterizedType)t).getActualTypeArguments()[index];
}
return Object.class;
}
protected FieldType assignSimpleType(Class<?> clzType, FieldImpl field) {
FieldType result = null;
if (clzType.isEnum()) {
result = FieldType.ENUM;
} else {
outer: for (FieldType type : FieldType.values()) {
Class<?>[] clzs = type.getClasses();
if (clzs == null)
continue;
for (Class<?> clz : clzs) {
if (clz.isAssignableFrom(clzType)) {
result = type;
if ((Number.class.isAssignableFrom(clzType) || Boolean.class.isAssignableFrom(clzType)) && !clz.isPrimitive() && field != null) {
field.setNullable(true);
}
break outer;
}
}
}
}
if (field != null) {
field.setTypeEnum(result);
}
return result;
}
protected io.github.ibuildthecloud.gdapi.annotation.Field getFieldAnnotation(PropertyDescriptor prop) {
Method readMethod = prop.getReadMethod();
Method writeMethod = prop.getWriteMethod();
io.github.ibuildthecloud.gdapi.annotation.Field f = null;
if (readMethod != null) {
f = readMethod.getAnnotation(io.github.ibuildthecloud.gdapi.annotation.Field.class);
}
if (f == null && writeMethod != null) {
f = writeMethod.getAnnotation(io.github.ibuildthecloud.gdapi.annotation.Field.class);
}
if (f == null) {
f = defaultField;
}
return f;
}
@PostConstruct
public void init() {
if (includeDefaultTypes) {
registerSchema(Schema.class);
registerSchema(ApiVersion.class);
registerSchema(ApiError.class);
registerSchema(Collection.class);
registerSchema(Resource.class);
}
for (Class<?> clz : types) {
registerSchema(clz);
}
for (String name : typeNames) {
registerSchema(name);
}
for (Schema schema : schemasList) {
parseSchema(schema.getId());
}
}
@Override
public List<Schema> listSchemas() {
return schemasList;
}
@Override
public Schema getSchema(String type) {
return schemasByName.get(lower(type));
}
@Override
public Class<?> getSchemaClass(String type) {
return typeToClass.get(lower(type));
}
protected String lower(String type) {
return type == null ? "" : type.toLowerCase();
}
public List<Class<?>> getTypes() {
return types;
}
public void setTypes(List<Class<?>> types) {
this.types = types;
}
public List<SchemaPostProcessor> getPostProcessors() {
return postProcessors;
}
@Inject
public void setPostProcessors(List<SchemaPostProcessor> postProcessors) {
this.postProcessors = CollectionUtils.orderList(SchemaPostProcessor.class, postProcessors);
}
public List<String> getTypeNames() {
return typeNames;
}
public void setTypeNames(List<String> typeNames) {
this.typeNames = typeNames;
}
public boolean isIncludeDefaultTypes() {
return includeDefaultTypes;
}
public void setIncludeDefaultTypes(boolean includeDefaultTypes) {
this.includeDefaultTypes = includeDefaultTypes;
}
public boolean isWritableByDefault() {
return writableByDefault;
}
public void setWritableByDefault(boolean writableByDefault) {
this.writableByDefault = writableByDefault;
}
public void setId(String id) {
this.id = id;
}
private static final class SchemaType {
String name;
String pluralName;
String parent;
Class<?> parentClass;
}
}