| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package org.apache.felix.schematizer.impl; |
| |
| import java.lang.reflect.Field; |
| import java.lang.reflect.Modifier; |
| import java.lang.reflect.Type; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.Comparator; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.LinkedHashMap; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.Map.Entry; |
| import java.util.stream.Collectors; |
| |
| import org.apache.felix.schematizer.Schematizer; |
| import org.osgi.framework.Bundle; |
| import org.osgi.framework.ServiceFactory; |
| import org.osgi.framework.ServiceRegistration; |
| import org.osgi.util.converter.Converter; |
| import org.osgi.util.converter.Converters; |
| import org.osgi.util.converter.TypeReference; |
| |
| import static org.apache.felix.schematizer.impl.Util.*; |
| |
| public class SchematizerImpl implements Schematizer, ServiceFactory<Schematizer> { |
| private final Map<String, SchemaImpl> schemas = new HashMap<>(); |
| private final Map<String, Map<String, Object>> typeRules = new HashMap<>(); |
| |
| @Override |
| public Schematizer getService( Bundle bundle, ServiceRegistration<Schematizer> registration ) { |
| return this; |
| } |
| |
| @Override |
| public void ungetService(Bundle bundle, ServiceRegistration<Schematizer> registration, Schematizer service) { |
| // For now, a brutish, simplistic version. If there is any change to the environment, just |
| // wipe the state and start over. |
| // |
| // TODO: something more precise, which will remove only the classes that are no longer valid (if that is possible). |
| schemas.clear(); |
| } |
| |
| @Override |
| public Schematizer type(String schemaName, String path, TypeReference<?> type) { |
| Map<String, Object> rules = rulesFor(schemaName); |
| // The internal implementation uses "" as the path for the root, |
| // but the API accepts "/". |
| path = "/".equals( path ) ? "" : path; |
| rules.put(path, type); |
| return this; |
| } |
| |
| @Override |
| public Schematizer type(String schemaName, String path, Class<?> cls) { |
| Map<String, Object> rules = rulesFor(schemaName); |
| // The internal implementation uses "" as the path for the root, |
| // but the API accepts "/". |
| path = "/".equals( path ) ? "" : path; |
| rules.put(path, cls); |
| return this; |
| } |
| |
| private Map<String, Object> rulesFor(String schemaName) { |
| if (!typeRules.containsKey(schemaName)) |
| typeRules.put(schemaName, new HashMap<>()); |
| |
| return typeRules.get(schemaName); |
| } |
| |
| @Override |
| public SchematizerImpl schematize(String schemaName, Object type) { |
| return schematize(schemaName, type, ""); |
| } |
| |
| public SchematizerImpl schematize(String schemaName, Object type, String context) { |
| // TODO: test to ensure that the schema is not already in the cache |
| Map<String, Object> rules = typeRules.get(schemaName); |
| rules = ( rules != null ) ? rules : Collections.emptyMap(); |
| SchemaImpl schema = internalSchematize(schemaName, type, context, rules, false); |
| schemas.put(schemaName, schema); |
| return this; |
| } |
| |
| private static SchemaImpl internalSchematize( |
| String schemaName, |
| Object unknownType, |
| String contextPath, |
| Map<String, Object> rules, |
| boolean isCollection) { |
| |
| TypeRefOrClass type = new TypeRefOrClass(unknownType, rules.get(contextPath)); |
| |
| if (asDTO(type.cls)) { |
| return schematizeDTO(schemaName, type, contextPath, rules, isCollection); |
| } |
| |
| return schematizeObject(schemaName, type.cls, contextPath, isCollection); |
| } |
| |
| private static SchemaImpl schematizeDTO( |
| String schemaName, |
| TypeRefOrClass type, |
| String contextPath, |
| Map<String, Object> rules, |
| boolean isCollection ) { |
| |
| SchemaImpl schema = new SchemaImpl(schemaName); |
| NodeImpl rootNode = new NodeImpl(contextPath, type.isTypeRef() ? type.typeRef : type.cls, false, contextPath + "/"); |
| schema.add(rootNode); |
| Map<String, NodeImpl> m = createMapFromDTO(schemaName, type, rules, contextPath); |
| m.values().stream() |
| .filter(v -> v.absolutePath().equals(rootNode.absolutePath() + v.name())) |
| .forEach(v -> rootNode.add(v)); |
| associateChildNodes( rootNode ); |
| schema.add(m); |
| return schema; |
| } |
| |
| private static SchemaImpl schematizeObject( |
| String schemaName, |
| Class<?> targetCls, |
| String contextPath, |
| boolean isCollection) { |
| |
| SchemaImpl schema = new SchemaImpl(schemaName); |
| NodeImpl node = new NodeImpl(contextPath, targetCls, isCollection, contextPath + "/"); |
| schema.add(node); |
| return schema; |
| } |
| |
| private static final Comparator<Entry<String, NodeImpl>> byPath = (e1, e2) -> e1.getValue().absolutePath().compareTo(e2.getValue().absolutePath()); |
| private static Map<String, NodeImpl> createMapFromDTO( |
| String schemaName, |
| TypeRefOrClass type, |
| Map<String, Object> rules, |
| String contextPath) { |
| Set<String> handledFields = new HashSet<>(); |
| |
| Map<String, NodeImpl> result = new HashMap<>(); |
| for (Field f : type.cls.getDeclaredFields()) { |
| handleField(schemaName, f, rules, handledFields, result, contextPath); |
| } |
| for (Field f : type.cls.getFields()) { |
| handleField(schemaName, f, rules, handledFields, result, contextPath); |
| } |
| |
| return result.entrySet().stream() |
| .sorted(byPath) |
| .collect(Collectors.toMap( |
| Entry::getKey, |
| Entry::getValue, |
| (e1, e2) -> e1, |
| LinkedHashMap::new)); |
| } |
| |
| @SuppressWarnings( { "unchecked", "rawtypes" } ) |
| private static void handleField( |
| String schemaName, |
| Field field, |
| Map<String, Object> rules, |
| Set<String> handledFields, |
| Map<String, NodeImpl> result, |
| String contextPath) { |
| if (Modifier.isStatic(field.getModifiers())) |
| return; |
| |
| String fieldName = field.getName(); |
| if (handledFields.contains(fieldName)) |
| return; // Field with this name was already handled |
| |
| try { |
| String path = contextPath + "/" + fieldName; |
| NodeImpl node; |
| if (rules.containsKey(path)) { |
| // The actual field. Since the type for this node is provided as a rule, we |
| // only need it to test whether or not it is a collection. |
| Class<?> actualFieldType = field.getType(); |
| boolean isCollection = Collection.class.isAssignableFrom(actualFieldType); |
| Class<?> ruleBasedClass = rawClassOf(rules.get(path)); |
| // This is the type we will persist in the Schema (as provided by the rules), NOT the "actual" field type. |
| SchemaImpl embedded = SchematizerImpl.internalSchematize(schemaName, ruleBasedClass, path, rules, isCollection); |
| Class<?> fieldClass = Util.primitiveToBoxed(ruleBasedClass); |
| TypeRefOrClass fieldType = new TypeRefOrClass(fieldClass,rules.get(path)); |
| if (isCollection) |
| node = new CollectionNode( |
| field.getName(), |
| fieldType.get(), |
| path, |
| (Class)actualFieldType); |
| else |
| node = new NodeImpl(fieldName, fieldType.get(), false, path); |
| Map<String, NodeImpl> allNodes = embedded.toMapInternal(); |
| allNodes.remove(path + "/"); |
| result.putAll(allNodes); |
| Map<String, NodeImpl> childNodes = extractChildren(path, allNodes); |
| node.add(childNodes); |
| } else { |
| Type fieldType = field.getType(); |
| Class<?> rawClass = rawClassOf(fieldType); |
| Class<?> fieldClass = primitiveToBoxed(rawClass); |
| |
| if (isCollectionType(fieldClass)) { |
| Class<?> collectionType = getCollectionTypeOf(field); |
| node = new CollectionNode( |
| field.getName(), |
| collectionType, |
| path, |
| (Class)fieldClass); |
| |
| if (asDTO(collectionType)) { |
| // newSchematizer.typeRules.put(path, rules); |
| // if (!rules.containsKey(path)) |
| // newSchematizer.rule(path, path, collectionType); |
| SchemaImpl embedded = new SchematizerImpl().schematize(path, collectionType, path).get(path); |
| Map<String, NodeImpl> allNodes = embedded.toMapInternal(); |
| allNodes.remove(path + "/"); |
| result.putAll(allNodes); |
| Map<String, NodeImpl> childNodes = extractChildren(path, allNodes); |
| node.add(childNodes); |
| } |
| } |
| else if (asDTO(fieldClass) || Util.isDTOType(fieldClass)) { |
| // newSchematizer.typeRules.put(path, rules); |
| // if (!rules.containsKey(path)) |
| // newSchematizer.rule(path, path, fieldClass); |
| SchemaImpl embedded = new SchematizerImpl().schematize(path, fieldClass, path).get(path); |
| node = new NodeImpl( |
| field.getName(), |
| fieldClass, |
| false, |
| path); |
| Map<String, NodeImpl> allNodes = embedded.toMapInternal(); |
| allNodes.remove(path + "/"); |
| result.putAll(allNodes); |
| Map<String, NodeImpl> childNodes = extractChildren(path, allNodes); |
| node.add(childNodes); |
| } else { |
| node = new NodeImpl( |
| field.getName(), |
| fieldClass, |
| false, |
| path); |
| } |
| } |
| |
| result.put(node.absolutePath(), node); |
| handledFields.add(fieldName); |
| } catch (Exception e) { |
| // Ignore this field |
| // TODO print warning?? |
| return; |
| } |
| } |
| |
| static private void associateChildNodes(NodeImpl rootNode) { |
| for (NodeImpl child: rootNode.childrenInternal().values()) { |
| child.parent(rootNode); |
| String fieldName = child.name(); |
| Class<?> parentClass = rawClassOf(rootNode.type()); |
| try { |
| Field field = parentClass.getField(fieldName); |
| child.field(field); |
| } catch ( NoSuchFieldException e ) { |
| e.printStackTrace(); |
| } |
| |
| associateChildNodes(child); |
| } |
| } |
| |
| @Override |
| public SchemaImpl get(String schemaName) { |
| return schemas.get(schemaName); |
| } |
| |
| @Override |
| public Converter converterFor(String schemaName) { |
| // ConverterBuilder b = new StandardConverter().newConverterBuilder(); |
| // Schema s = schemas.get(schemaName); |
| // RuleExtractor ex = new RuleExtractor(); |
| // s.visit( ex ); |
| // ex.rules().stream().forEach( rule -> b.rule(rule) ); |
| // return b.build(); |
| return Converters |
| .newConverterBuilder() |
| .rule(new SchemaBasedConverter<Object>(schemas.get(schemaName))) |
| .build(); |
| } |
| |
| // private static class RuleExtractor implements NodeVisitor { |
| // private final List<TargetRule<?>> rules = new ArrayList<>(); |
| // |
| // @Override |
| // public void apply(Node node) { |
| // rules.add(new DTOTargetRule<Type>(node)); |
| // } |
| // |
| // List<TargetRule<?>> rules() { |
| // return rules; |
| // } |
| // } |
| // private static class DTOTargetRule<T> implements TargetRule<T> { |
| // private final Type type; |
| // |
| // public DTOTargetRule(Node node) { |
| // if (node.isCollection()) |
| // type = new CollectionType(node.collectionType(), new TypeRefOrClass(node.type())); |
| // else |
| // type = node.type(); |
| // } |
| // |
| // @Override |
| // public ConverterFunction<T> getFunction() { |
| // return (obj,t) -> { |
| // TypeRefOrClass type = null; |
| // if(t instanceof CollectionType) { |
| // return convertCollection((Collection<?>)obj, (CollectionType)t); |
| // } else { |
| // type = new TypeRefOrClass(t); |
| // return convertObject(obj,type); |
| // } |
| // }; |
| // } |
| // |
| // @SuppressWarnings( "unchecked" ) |
| // private T convertCollection(Collection<?> c, CollectionType type) { |
| // Collection<Object> copy = newCollection(type); |
| // for(Object obj : c) |
| // copy.add((Object)convertObject(obj, type.itemType)); |
| // return (T)copy; |
| // } |
| // |
| // private Collection<Object> newCollection(CollectionType type) { |
| // // TODO what else? |
| // return new ArrayList<>(); |
| // } |
| // |
| // @SuppressWarnings( "unchecked" ) |
| // private T convertObject(Object obj, TypeRefOrClass type) { |
| // Converter c = new StandardConverter(); |
| // if (asDTO(type.getClassType())) |
| // if(type.isTypeRef()) |
| // return c.convert(obj).targetAsDTO().to((TypeReference<T>)type.getTypeRef()); |
| // else |
| // return c.convert(obj).targetAsDTO().to(type.getType()); |
| // return c.convert(obj).targetAsDTO().to(type.getType()); |
| // } |
| // |
| // @Override |
| // public Type getTargetType() { |
| // if (type instanceof CollectionType) |
| // return ((CollectionType)type).collectionType; |
| // return type; |
| // } |
| // }; |
| |
| static class TypeRefOrClass { |
| TypeReference<?> typeRef; |
| Class<?> cls; |
| |
| public TypeRefOrClass(Object type, Object ruleBasedType) { |
| this(ruleBasedType != null ? ruleBasedType : type); |
| } |
| |
| public TypeRefOrClass(Object type) { |
| typeRef = (TypeReference<?>)(typeReferenceOf(type)); |
| if (typeRef != null ) |
| cls = rawClassOf(typeRef); |
| else |
| cls = rawClassOf(type); |
| } |
| |
| boolean isTypeRef() { |
| return typeRef != null; |
| } |
| |
| TypeReference<?> getTypeRef() { |
| return typeRef; |
| } |
| |
| Object get() { |
| if (typeRef != null ) |
| return typeRef; |
| return cls; |
| } |
| |
| Type getType() { |
| if (typeRef != null) |
| return typeRef.getType(); |
| return cls; |
| } |
| |
| Class<?> getClassType() { |
| Type t = getType(); |
| if (t instanceof Class) |
| return (Class<?>)t; |
| return t.getClass(); |
| } |
| } |
| |
| static class CollectionType implements Type { |
| Class<? extends Collection<?>> collectionType; |
| TypeRefOrClass itemType; |
| |
| public CollectionType(Class<? extends Collection<?>> aCollectionType, TypeRefOrClass anItemType) { |
| collectionType = aCollectionType; |
| itemType = anItemType; |
| } |
| } |
| } |