Author: cbrisson
Date: Thu Apr 18 15:05:51 2019
New Revision: 1857756

URL: http://svn.apache.org/viewvc?rev=1857756&view=rev
Log:
[tools/model] Initial import: tailored ConfigDigester

Added:
    
velocity/tools/branches/model/velocity-tools-model/src/main/java/org/apache/velocity/tools/model/config/ConfigDigester.java

Added: 
velocity/tools/branches/model/velocity-tools-model/src/main/java/org/apache/velocity/tools/model/config/ConfigDigester.java
URL: 
http://svn.apache.org/viewvc/velocity/tools/branches/model/velocity-tools-model/src/main/java/org/apache/velocity/tools/model/config/ConfigDigester.java?rev=1857756&view=auto
==============================================================================
--- 
velocity/tools/branches/model/velocity-tools-model/src/main/java/org/apache/velocity/tools/model/config/ConfigDigester.java
 (added)
+++ 
velocity/tools/branches/model/velocity-tools-model/src/main/java/org/apache/velocity/tools/model/config/ConfigDigester.java
 Thu Apr 18 15:05:51 2019
@@ -0,0 +1,411 @@
+package org.apache.velocity.tools.model.config;
+/*
+ * 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.
+ */
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.velocity.tools.ClassUtils;
+import org.apache.velocity.tools.config.ConfigurationException;
+import org.apache.velocity.tools.model.Attribute;
+import org.apache.velocity.tools.model.Entity;
+import org.apache.velocity.tools.model.Model;
+import org.apache.velocity.tools.model.impl.AttributeHolder;
+import org.apache.velocity.tools.model.impl.BaseAttribute;
+import org.apache.velocity.tools.model.util.TypeUtils;
+import org.w3c.dom.Attr;
+import org.w3c.dom.Element;
+import org.w3c.dom.NamedNodeMap;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Method;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.Stack;
+import java.util.TreeMap;
+import java.util.function.Predicate;
+
+/**
+ * <p>A tailored minimalistic digester for XML configuration reading of the 
model tree.</p>
+ *
+ * @author Claude Brisson
+ */
+
+// TODO use annotations
+
+public class ConfigDigester
+{
+    public ConfigDigester(Element doc, Object bean)
+    {
+        xmlPath.push(doc);
+        beanStack.push(bean);
+    }
+
+    public void process() throws Exception
+    {
+        initReflection();
+        useModelNamespace = isDeclaringNamespace(xmlPath.peek(), "model");
+        recurseProcessing();
+    }
+
+    private void initReflection() throws Exception
+    {
+        addAttributeQueryPart = 
BaseAttribute.class.getDeclaredMethod("addQueryPart", String.class);
+        addAttributeQueryPart.setAccessible(true);
+        addAttributeParameter = 
BaseAttribute.class.getDeclaredMethod("addParameter", String.class);
+        addAttributeParameter.setAccessible(true);
+        addAttribute = AttributeHolder.class.getDeclaredMethod("addAttribute", 
Attribute.class);
+        addAttribute.setAccessible(true);
+    }
+
+    private void recurseProcessing() throws Exception
+    {
+        Element element = xmlPath.peek();
+        String tag = element.getTagName();
+        Object bean = beanStack.peek();
+
+        Map<String, Object> attrMap = getAttributesMap(element);
+        setProperties(bean, attrMap);
+
+        NodeList children = element.getChildNodes();
+        for (int i = 0; i <children.getLength(); ++i)
+        {
+            Node child = children.item(i);
+            Object childBean = null;
+
+            if (bean instanceof Attribute)
+            {
+                Attribute attribute = (Attribute)bean;
+                addAttributePart(attribute, child);
+            }
+            else if (child.getNodeType() == Node.ELEMENT_NODE)
+            {
+                handleChildElement(bean, (Element)child);
+            }
+        }
+        xmlPath.pop();
+        beanStack.pop();
+    }
+
+    private void addAttributePart(Attribute attribute, Node child) throws 
Exception
+    {
+        switch (child.getNodeType())
+        {
+            case Node.TEXT_NODE:
+            {
+                String queryPart = child.getNodeValue();
+                addAttributeQueryPart.invoke(attribute, queryPart);
+                break;
+            }
+            case Node.ELEMENT_NODE:
+            {
+                String paramName = child.getLocalName();
+                addAttributeParameter.invoke(attribute, paramName);
+                break;
+            }
+        }
+    }
+
+    private void handleChildElement(Object parentBean, Element childElement) 
throws Exception
+    {
+        String childName = childElement.getLocalName();
+        Object childBean;
+        if (isAttributeResult(childElement))
+        {
+            String attributeClassName = modelPackage + "." + 
StringUtils.capitalize(childName);
+            Class attributeClass = null;
+            try
+            {
+                // first try with exact child name capitalized
+                attributeClass = ClassUtils.getClass(attributeClassName);
+            }
+            catch (ClassNotFoundException cnfe)
+            {
+                // second try with 'Attribute' postfix
+                attributeClassName += "Attribute";
+                attributeClass = ClassUtils.getClass(attributeClassName);
+            }
+            String attributeName = childElement.getAttribute("name");
+            if (attributeName == null || attributeName.length() == 0)
+                throw new ConfigurationException("attribute without name:" + 
childElement.toString());
+            childElement.removeAttribute("name");
+            childBean = createChildInstance(attributeName, attributeClass);
+            addAttribute.invoke(parentBean, childBean);
+        }
+        else
+        {
+            childBean = createChildInstance(childName, Entity.class);
+            ((Model)parentBean).addEntity((Entity)childBean);
+        }
+
+        xmlPath.push(childElement);
+        beanStack.push(childBean);
+        recurseProcessing();
+    }
+
+    private Map<String, Object> getAttributesMap(Element element)
+    {
+        Map<String, Object> attrMap = new TreeMap<>();
+        NamedNodeMap attributes = element.getAttributes();
+        for (int i = 0; i < attributes.getLength(); ++i)
+        {
+            Attr attribute = (Attr)attributes.item(i);
+            String attributeName = attribute.getName();
+            // filter out xmlns: etc
+            if (attributeName.contains(":")) // the only ':' used in model DTD 
are for tags
+            {
+                // not for us
+                continue;
+            }
+            String value = attribute.getValue();
+
+            // handle subproperties
+            int dot = attributeName.indexOf('.');
+            if (dot > 0)
+            {
+                String prop = attributeName.substring(0, dot);
+                Map<String, Object> subProps = null;
+                Object sub = attrMap.get(prop);
+                if (sub == null)
+                {
+                    subProps = new TreeMap<String, Object>();
+                    attrMap.put(prop, subProps);
+                }
+                else if (sub instanceof Map)
+                {
+                    subProps = (Map<String, Object>)sub;
+                }
+                else
+                {
+                    throw new ConfigurationException("cannot mix values and 
subproperties for property: " + prop);
+                }
+                subProps.put(attributeName.substring(dot + 1), value);
+            }
+            else
+            {
+                attrMap.put(attributeName, value);
+            }
+        }
+        return attrMap;
+    }
+
+    public static void setProperties(Object bean, Map properties) throws 
Exception
+    {
+        Map<String, Map<String, Object>> subProps = new TreeMap<>();
+        for (Map.Entry entry : (Set<Map.Entry>)properties.entrySet())
+        {
+            String key = (String)entry.getKey();
+            Object value = entry.getValue();
+            int dot;
+            if ((dot = key.indexOf(".")) != -1)
+            {
+                String subkey = key.substring(0, dot);
+                Map<String, Object> submap = subProps.get(subkey);
+                if (submap == null)
+                {
+                    submap = new TreeMap<String, Object>();
+                    subProps.put(subkey, submap);
+                }
+                submap.put(key.substring(dot + 1), value);
+            }
+            else
+            {
+                setProperty(bean, (String)entry.getKey(), entry.getValue());
+            }
+        }
+        for (Map.Entry<String, Map<String, Object>> entry : 
subProps.entrySet())
+        {
+            setProperty(bean, entry.getKey(), entry.getValue());
+        }
+    }
+
+    public static void setProperty(Object bean, String name, Object value) 
throws Exception
+    {
+        if (value == null)
+        {
+            return;
+        }
+
+        // search for a getter
+        if (value instanceof Map)
+        {
+            String getterName = getGetterName(name);
+            Method getter = ClassUtils.findGetter(getterName, bean.getClass(), 
false);
+            if (getter != null)
+            {
+                Object subBean = getter.invoke(bean);
+                setProperties(subBean, (Map)value);
+                return;
+            }
+        }
+
+        // search for a setter
+        String setterName = getSetterName(name);
+        Method setter = ClassUtils.findSetter(setterName, bean.getClass(), 
ConfigDigester::isScalarType, false);
+        if (setter == null)
+        {
+            // search for a map-like put() method
+            Class clazz = bean.getClass();
+            do
+            {
+                for (Method method : clazz.getDeclaredMethods())
+                {
+                    // prefix matching: we allow a method name like 
setWriteAccess for a parameter like write="..."
+                    if (method.getParameterCount() == 2 && 
method.getName().equals("put") && method.getParameterTypes()[0] == String.class)
+                    {
+                        setter = method;
+                    }
+                }
+                clazz = clazz.getSuperclass();
+            }
+            while (setter == null && clazz != Object.class);
+        }
+        if (setter == null)
+        {
+            throw new ConfigurationException("no setter for preperty " + name 
+ " on class " + bean.getClass());
+        }
+        setter.setAccessible(true);
+        Object argument;
+        Class paramClass = 
setter.getParameterTypes()[setter.getParameterCount() - 1];
+        if (paramClass == String.class)
+        {
+            argument = value;
+        }
+        else if (paramClass == Boolean.TYPE)
+        {
+            argument = TypeUtils.toBoolean(value);
+        }
+        else if (Enum.class.isAssignableFrom(paramClass) && value instanceof 
String)
+        {
+            argument = Enum.valueOf(paramClass, ((String)value).toUpperCase());
+        }
+        else
+        {
+            throw new ConfigurationException("cannot convert value to setter 
argument: " + setterName + "(" + paramClass + ")");
+        }
+        switch (setter.getParameterCount())
+        {
+            case 1:
+                setter.invoke(bean, argument);
+                break;
+            case 2:
+                setter.invoke(bean, name, argument);
+                break;
+            default:
+                throw new ConfigurationException("oops, unhandled case");
+        }
+
+    }
+
+    private Object createChildInstance(String name, Class clazz) throws 
Exception
+    {
+        Object parent = beanStack.peek();
+        Class parentClass = parent.getClass();
+        Constructor constructor = null;
+        do
+        {
+            try
+            {
+                constructor = clazz.getConstructor(String.class, parentClass);
+            }
+            catch (NoSuchMethodException nsme)
+            {
+            }
+            parentClass = parentClass.getSuperclass();
+        }
+        while (parentClass != null && constructor == null);
+        if (constructor == null)
+        {
+            throw new ConfigurationException("no appropriate constructor for 
class " + clazz.getName());
+        }
+        constructor.setAccessible(true);
+        return constructor.newInstance(name, parent);
+    }
+
+    private Object createChildInstance(String name, String className) throws 
Exception
+    {
+        return createChildInstance(name, ClassUtils.getClass(className));
+    }
+
+    private boolean isAttributeResult(Element element)
+    {
+        if (useModelNamespace)
+        {
+            return 
Constants.MODEL_NAMESPACE_URI.equals(element.getNamespaceURI()) && 
attributeResults.contains(element.getLocalName());
+        }
+        else
+        {
+            return attributeResults.contains(element.getTagName());
+        }
+    }
+
+    private boolean isDeclaringNamespace(Element root, String namespace)
+    {
+        namespace = "xmlns:" + namespace;
+        NamedNodeMap attributes = root.getAttributes();
+        for (int i = 0; i < attributes.getLength(); ++i)
+        {
+            Attr attribute = (Attr)attributes.item(i);
+            if (namespace.equals(attribute.getNodeName())) return true;
+        }
+        return false;
+    }
+
+    public static String getSetterName(String name)
+    {
+        String[] parts = StringUtils.split(name, "_-.");
+        StringBuilder builder = new StringBuilder("set");
+        for (String part : parts)
+        {
+            builder.append(StringUtils.capitalize(part));
+        }
+        return builder.toString();
+    }
+
+    public static String getGetterName(String name)
+    {
+        String[] parts = StringUtils.split(name, "_-");
+        StringBuilder builder = new StringBuilder("get");
+        for (String part : parts)
+        {
+            builder.append(StringUtils.capitalize(part));
+        }
+        return builder.toString();
+    }
+
+    private static Set<Class> scalarTypes = new HashSet<>(Arrays.asList(
+        String.class, Boolean.TYPE, Short.TYPE, Integer.TYPE, Long.TYPE, 
Float.TYPE, Double.TYPE
+    ));
+
+    public static boolean isScalarType(Class typeClass)
+    {
+        return scalarTypes.contains(typeClass) || 
Enum.class.isAssignableFrom(typeClass);
+    }
+
+    private Stack<Element> xmlPath = new Stack<>();
+    private Stack<Object> beanStack = new Stack<>();
+    private boolean useModelNamespace = false;
+    private Method addAttributeQueryPart = null, addAttributeParameter = null, 
addAttribute = null;
+
+    private static final String modelPackage = 
"org.apache.velocity.tools.model";
+    // for now, explicit types as tags (like <int name="...">) aren't taken 
into account
+    private static final Set<String> attributeResults = new 
HashSet<>(Arrays.asList("scalar", /* "string", "boolean", "int", "long", 
"float", "double",*/ "row", "rowset", "action", "transaction"));
+}


Reply via email to