com.datatorrent.stram.webapp.OperatorDiscoverer.java Source code

Java tutorial

Introduction

Here is the source code for com.datatorrent.stram.webapp.OperatorDiscoverer.java

Source

/**
 * 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 com.datatorrent.stram.webapp;

import com.datatorrent.api.Operator;
import com.datatorrent.netlet.util.DTThrowable;
import com.datatorrent.stram.util.ObjectMapperFactory;
import com.datatorrent.stram.webapp.TypeDiscoverer.UI_TYPE;
import com.datatorrent.stram.webapp.TypeGraph.TypeGraphVertex;
import com.datatorrent.stram.webapp.asm.CompactAnnotationNode;
import com.datatorrent.stram.webapp.asm.CompactFieldNode;
import com.google.common.base.Predicate;
import com.google.common.base.Splitter;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;

import java.beans.*;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.*;
import java.net.*;
import java.util.*;
import java.util.Map.Entry;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.xml.parsers.*;

import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.ClassUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.text.WordUtils;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jettison.json.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

/**
 * <p>OperatorDiscoverer class.</p>
 * Discover Operators.
 * Warning: Using this class may cause classloader leakage, depending on the classes it loads.
 *
 * @since 0.3.2
 */
public class OperatorDiscoverer {
    public static final String GENERATED_CLASSES_JAR = "_generated-classes.jar";
    private Set<String> operatorClassNames;
    private static final Logger LOG = LoggerFactory.getLogger(OperatorDiscoverer.class);
    private final List<String> pathsToScan = new ArrayList<String>();
    private final ClassLoader classLoader;
    private static final String DT_OPERATOR_DOCLINK_PREFIX = "https://www.datatorrent.com/docs/apidocs/index.html";
    public static final String PORT_TYPE_INFO_KEY = "portTypeInfo";
    private final TypeGraph typeGraph = TypeGraphFactory.createTypeGraphProtoType();

    private static final Pattern WHITESPACE_PATTERN = Pattern.compile("\\s+?");

    private static final String SCHEMA_REQUIRED_KEY = "schemaRequired";

    private final Map<String, OperatorClassInfo> classInfo = new HashMap<String, OperatorClassInfo>();

    private static class OperatorClassInfo {
        String comment;
        final Map<String, String> tags = new HashMap<String, String>();
        final Map<String, MethodInfo> getMethods = Maps.newHashMap();
        final Map<String, MethodInfo> setMethods = Maps.newHashMap();
        final Map<String, String> fields = new HashMap<String, String>();
    }

    private static class MethodInfo {
        Map<String, String> descriptions = Maps.newHashMap();
        Map<String, String> useSchemas = Maps.newHashMap();
        String comment;
        boolean omitFromUI;
    }

    enum MethodTagType {
        USE_SCHEMA("@useSchema"), DESCRIPTION("@description"), OMIT_FROM_UI("@omitFromUI");

        private static final Map<String, MethodTagType> TAG_TEXT_MAPPING = Maps.newHashMap();

        static {
            for (MethodTagType type : MethodTagType.values()) {
                TAG_TEXT_MAPPING.put(type.tag, type);
            }
        }

        private final String tag;

        MethodTagType(String tag) {
            this.tag = tag;
        }

        static MethodTagType from(String tag) {
            return TAG_TEXT_MAPPING.get(tag);
        }
    }

    private class JavadocSAXHandler extends DefaultHandler {

        private String className = null;
        private OperatorClassInfo oci = null;
        private StringBuilder comment;
        private String fieldName = null;
        private String methodName = null;
        private final Pattern getterPattern = Pattern.compile("(?:is|get)[A-Z].*");
        private final Pattern setterPattern = Pattern.compile("(?:set)[A-Z].*");

        private boolean isGetter(String methodName) {
            return getterPattern.matcher(methodName).matches();
        }

        private boolean isSetter(String methodName) {
            return setterPattern.matcher(methodName).matches();
        }

        @Override
        public void startElement(String uri, String localName, String qName, Attributes attributes)
                throws SAXException {
            if (qName.equalsIgnoreCase("class")) {
                className = attributes.getValue("qualified");
                oci = new OperatorClassInfo();
            } else if (qName.equalsIgnoreCase("comment")) {
                comment = new StringBuilder();
            } else if (qName.equalsIgnoreCase("tag")) {
                if (oci != null) {
                    String tagName = attributes.getValue("name");
                    String tagText = attributes.getValue("text").trim();
                    if (methodName != null) {
                        boolean lGetterCheck = isGetter(methodName);
                        boolean lSetterCheck = !lGetterCheck && isSetter(methodName);

                        if (lGetterCheck || lSetterCheck) {
                            MethodTagType type = MethodTagType.from(tagName);
                            if (type != null) {
                                addTagToMethod(lGetterCheck ? oci.getMethods : oci.setMethods, tagText, type);
                            }
                        }
                        //            if ("@return".equals(tagName) && isGetter(methodName)) {
                        //              oci.getMethods.put(methodName, tagText);
                        //            }
                        //do nothing
                    } else if (fieldName != null) {
                        // do nothing
                    } else {
                        oci.tags.put(tagName, tagText);
                    }
                }
            } else if (qName.equalsIgnoreCase("field")) {
                fieldName = attributes.getValue("name");
            } else if (qName.equalsIgnoreCase("method")) {
                methodName = attributes.getValue("name");
            }
        }

        private void addTagToMethod(Map<String, MethodInfo> methods, String tagText, MethodTagType tagType) {
            MethodInfo mi = methods.get(methodName);
            if (mi == null) {
                mi = new MethodInfo();
                methods.put(methodName, mi);
            }
            if (tagType == MethodTagType.OMIT_FROM_UI) {
                mi.omitFromUI = true;
                return;
            }
            String[] tagParts = Iterables.toArray(
                    Splitter.on(WHITESPACE_PATTERN).trimResults().omitEmptyStrings().limit(2).split(tagText),
                    String.class);
            if (tagParts.length == 2) {
                if (tagType == MethodTagType.DESCRIPTION) {
                    mi.descriptions.put(tagParts[0], tagParts[1]);
                } else {
                    mi.useSchemas.put(tagParts[0], tagParts[1]);
                }
            }
        }

        @Override
        public void endElement(String uri, String localName, String qName) throws SAXException {
            if (qName.equalsIgnoreCase("class")) {
                classInfo.put(className, oci);
                className = null;
                oci = null;
            } else if (qName.equalsIgnoreCase("comment") && oci != null) {
                if (methodName != null) {
                    // do nothing
                    if (isGetter(methodName)) {
                        MethodInfo mi = oci.getMethods.get(methodName);
                        if (mi == null) {
                            mi = new MethodInfo();
                            oci.getMethods.put(methodName, mi);
                        }
                        mi.comment = comment.toString();
                    } else if (isSetter(methodName)) {
                        MethodInfo mi = oci.setMethods.get(methodName);
                        if (mi == null) {
                            mi = new MethodInfo();
                            oci.setMethods.put(methodName, mi);
                        }
                        mi.comment = comment.toString();
                    }
                } else if (fieldName != null) {
                    oci.fields.put(fieldName, comment.toString());
                } else {
                    oci.comment = comment.toString();
                }
                comment = null;
            } else if (qName.equalsIgnoreCase("field")) {
                fieldName = null;
            } else if (qName.equalsIgnoreCase("method")) {
                methodName = null;
            }
        }

        @Override
        public void characters(char ch[], int start, int length) throws SAXException {
            if (comment != null) {
                comment.append(ch, start, length);
            }
        }
    }

    public OperatorDiscoverer() {
        classLoader = ClassLoader.getSystemClassLoader();
    }

    public OperatorDiscoverer(String[] jars) {
        URL[] urls = new URL[jars.length];
        for (int i = 0; i < jars.length; i++) {
            pathsToScan.add(jars[i]);
            try {
                urls[i] = new URL("file://" + jars[i]);
            } catch (MalformedURLException ex) {
                throw new RuntimeException(ex);
            }
        }
        classLoader = new URLClassLoader(urls, ClassLoader.getSystemClassLoader());
    }

    private void loadOperatorClass() {
        buildTypeGraph();
        operatorClassNames = typeGraph.getAllDTInstantiableOperators();
    }

    @SuppressWarnings("unchecked")
    public void addDefaultValue(String className, JSONObject oper) throws Exception {
        ObjectMapper defaultValueMapper = ObjectMapperFactory.getOperatorValueSerializer();
        Class<? extends Operator> clazz = (Class<? extends Operator>) classLoader.loadClass(className);
        if (clazz != null) {
            Operator operIns = clazz.newInstance();
            String s = defaultValueMapper.writeValueAsString(operIns);
            oper.put("defaultValue", new JSONObject(s).get(className));
        }
    }

    public void buildTypeGraph() {
        Map<String, JarFile> openJarFiles = new HashMap<String, JarFile>();
        Map<String, File> openClassFiles = new HashMap<String, File>();
        // use global cache to load resource in/out of the same jar as the classes
        Set<String> resourceCacheSet = new HashSet<>();
        try {
            for (String path : pathsToScan) {
                File f = null;
                try {
                    f = new File(path);
                    if (!f.exists() || f.isDirectory()
                            || (!f.getName().endsWith("jar") && !f.getName().endsWith("class"))) {
                        continue;
                    }
                    if (GENERATED_CLASSES_JAR.equals(f.getName())) {
                        continue;
                    }
                    if (f.getName().endsWith("class")) {
                        typeGraph.addNode(f);
                        openClassFiles.put(path, f);
                    } else {
                        JarFile jar = new JarFile(path);
                        openJarFiles.put(path, jar);
                        java.util.Enumeration<JarEntry> entriesEnum = jar.entries();
                        while (entriesEnum.hasMoreElements()) {
                            final java.util.jar.JarEntry jarEntry = entriesEnum.nextElement();
                            String entryName = jarEntry.getName();
                            if (jarEntry.isDirectory()) {
                                continue;
                            }
                            if (entryName.endsWith("-javadoc.xml")) {
                                try {
                                    processJavadocXml(jar.getInputStream(jarEntry));
                                    // break;
                                } catch (Exception ex) {
                                    LOG.warn("Cannot process javadoc {} : ", entryName, ex);
                                }
                            } else if (entryName.endsWith(".class")) {
                                TypeGraph.TypeGraphVertex newNode = typeGraph.addNode(jarEntry, jar);
                                // check if any visited resources belong to this type
                                for (Iterator<String> iter = resourceCacheSet.iterator(); iter.hasNext();) {
                                    String entry = iter.next();
                                    if (entry.startsWith(entryName.substring(0, entryName.length() - 6))) {
                                        newNode.setHasResource(true);
                                        iter.remove();
                                    }
                                }
                            } else {
                                String className = entryName;
                                boolean foundClass = false;
                                // check if this resource belongs to any visited type
                                while (className.contains("/")) {
                                    className = className.substring(0, className.lastIndexOf('/'));
                                    TypeGraph.TypeGraphVertex tgv = typeGraph.getNode(className.replace('/', '.'));
                                    if (tgv != null) {
                                        tgv.setHasResource(true);
                                        foundClass = true;
                                        break;
                                    }
                                }
                                if (!foundClass) {
                                    resourceCacheSet.add(entryName);
                                }
                            }
                        }
                    }
                } catch (IOException ex) {
                    LOG.warn("Cannot process file {}", f, ex);
                }
            }

            typeGraph.trim();

        } finally {
            for (Entry<String, JarFile> entry : openJarFiles.entrySet()) {
                try {
                    entry.getValue().close();
                } catch (IOException e) {
                    DTThrowable.wrapIfChecked(e);
                }
            }
        }
    }

    private void processJavadocXml(InputStream is) throws ParserConfigurationException, SAXException, IOException {
        SAXParserFactory saxParserFactory = SAXParserFactory.newInstance();
        saxParserFactory.newSAXParser().parse(is, new JavadocSAXHandler());
    }

    public Set<String> getOperatorClasses(String parent, String searchTerm) throws ClassNotFoundException {
        if (CollectionUtils.isEmpty(operatorClassNames)) {
            loadOperatorClass();
        }
        if (parent == null) {
            parent = Operator.class.getName();
        } else {
            if (!typeGraph.isAncestor(Operator.class.getName(), parent)) {
                throw new IllegalArgumentException("Argument must be a subclass of Operator class");
            }
        }

        Set<String> filteredClass = Sets.filter(operatorClassNames, new Predicate<String>() {
            @Override
            public boolean apply(String className) {
                OperatorClassInfo oci = classInfo.get(className);
                return oci == null || !oci.tags.containsKey("@omitFromUI");
            }
        });

        if (searchTerm == null && parent.equals(Operator.class.getName())) {
            return filteredClass;
        }

        if (searchTerm != null) {
            searchTerm = searchTerm.toLowerCase();
        }

        Set<String> result = new HashSet<String>();
        for (String clazz : filteredClass) {
            if (parent.equals(Operator.class.getName()) || typeGraph.isAncestor(parent, clazz)) {
                if (searchTerm == null) {
                    result.add(clazz);
                } else {
                    if (clazz.toLowerCase().contains(searchTerm)) {
                        result.add(clazz);
                    } else {
                        OperatorClassInfo oci = classInfo.get(clazz);
                        if (oci != null) {
                            if (oci.comment != null && oci.comment.toLowerCase().contains(searchTerm)) {
                                result.add(clazz);
                            } else {
                                for (Map.Entry<String, String> entry : oci.tags.entrySet()) {
                                    if (entry.getValue().toLowerCase().contains(searchTerm)) {
                                        result.add(clazz);
                                        break;
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
        return result;
    }

    @SuppressWarnings("unchecked")
    public Class<? extends Operator> getOperatorClass(String className) throws ClassNotFoundException {
        if (CollectionUtils.isEmpty(operatorClassNames)) {
            loadOperatorClass();
        }

        Class<?> clazz = classLoader.loadClass(className);

        if (!Operator.class.isAssignableFrom(clazz)) {
            throw new IllegalArgumentException("Argument must be a subclass of Operator class");
        }
        return (Class<? extends Operator>) clazz;
    }

    public JSONObject describeOperator(String clazz) throws Exception {
        TypeGraphVertex tgv = typeGraph.getTypeGraphVertex(clazz);
        if (tgv.isInstantiable()) {
            JSONObject response = new JSONObject();
            JSONArray inputPorts = new JSONArray();
            JSONArray outputPorts = new JSONArray();
            // Get properties from ASM

            JSONObject operatorDescriptor = describeClassByASM(clazz);
            JSONArray properties = operatorDescriptor.getJSONArray("properties");

            properties = enrichProperties(clazz, properties);

            JSONArray portTypeInfo = operatorDescriptor.getJSONArray("portTypeInfo");

            List<CompactFieldNode> inputPortfields = typeGraph.getAllInputPorts(clazz);
            List<CompactFieldNode> outputPortfields = typeGraph.getAllOutputPorts(clazz);

            try {
                for (CompactFieldNode field : inputPortfields) {
                    JSONObject inputPort = setFieldAttributes(clazz, field);
                    if (!inputPort.has("optional")) {
                        inputPort.put("optional", false); // input port that is not annotated is default to be not optional
                    }
                    if (!inputPort.has(SCHEMA_REQUIRED_KEY)) {
                        inputPort.put(SCHEMA_REQUIRED_KEY, false);
                    }
                    inputPorts.put(inputPort);
                }

                for (CompactFieldNode field : outputPortfields) {
                    JSONObject outputPort = setFieldAttributes(clazz, field);

                    if (!outputPort.has("optional")) {
                        outputPort.put("optional", true); // output port that is not annotated is default to be optional
                    }
                    if (!outputPort.has("error")) {
                        outputPort.put("error", false);
                    }
                    if (!outputPort.has(SCHEMA_REQUIRED_KEY)) {
                        outputPort.put(SCHEMA_REQUIRED_KEY, false);
                    }
                    outputPorts.put(outputPort);
                }

                response.put("name", clazz);
                response.put("properties", properties);
                response.put(PORT_TYPE_INFO_KEY, portTypeInfo);
                response.put("inputPorts", inputPorts);
                response.put("outputPorts", outputPorts);

                OperatorClassInfo oci = classInfo.get(clazz);

                if (oci != null) {
                    if (oci.comment != null) {
                        String[] descriptions;
                        // first look for a <p> tag
                        String keptPrefix = "<p>";
                        descriptions = oci.comment.split("<p>", 2);
                        if (descriptions.length == 0) {
                            keptPrefix = "";
                            // if no <p> tag, then look for a blank line
                            descriptions = oci.comment.split("\n\n", 2);
                        }
                        if (descriptions.length > 0) {
                            response.put("shortDesc", descriptions[0]);
                        }
                        if (descriptions.length > 1) {
                            response.put("longDesc", keptPrefix + descriptions[1]);
                        }
                    }
                    response.put("category", oci.tags.get("@category"));
                    String displayName = oci.tags.get("@displayName");
                    if (displayName == null) {
                        displayName = decamelizeClassName(ClassUtils.getShortClassName(clazz));
                    }
                    response.put("displayName", displayName);
                    String tags = oci.tags.get("@tags");
                    if (tags != null) {
                        JSONArray tagArray = new JSONArray();
                        for (String tag : StringUtils.split(tags, ',')) {
                            tagArray.put(tag.trim().toLowerCase());
                        }
                        response.put("tags", tagArray);
                    }
                    String doclink = oci.tags.get("@doclink");
                    if (doclink != null) {
                        response.put("doclink", doclink + "?" + getDocName(clazz));
                    } else if (clazz.startsWith("com.datatorrent.lib.")
                            || clazz.startsWith("com.datatorrent.contrib.")) {
                        response.put("doclink", DT_OPERATOR_DOCLINK_PREFIX + "?" + getDocName(clazz));
                    }
                }
            } catch (JSONException ex) {
                throw new RuntimeException(ex);
            }
            return response;
        } else {
            throw new UnsupportedOperationException();
        }
    }

    private JSONObject setFieldAttributes(String clazz, CompactFieldNode field) throws JSONException {
        JSONObject port = new JSONObject();
        port.put("name", field.getName());

        TypeGraphVertex tgv = typeGraph.getTypeGraphVertex(clazz);
        putFieldDescription(field, port, tgv);

        List<CompactAnnotationNode> annotations = field.getVisibleAnnotations();
        CompactAnnotationNode firstAnnotation;
        if (annotations != null && !annotations.isEmpty()
                && (firstAnnotation = field.getVisibleAnnotations().get(0)) != null) {
            for (Map.Entry<String, Object> entry : firstAnnotation.getAnnotations().entrySet()) {
                port.put(entry.getKey(), entry.getValue());
            }
        }

        return port;
    }

    private void putFieldDescription(CompactFieldNode field, JSONObject port, TypeGraphVertex tgv)
            throws JSONException {
        OperatorClassInfo oci = classInfo.get(tgv.typeName);
        if (oci != null) {
            String fieldDesc = oci.fields.get(field.getName());
            if (fieldDesc != null) {
                port.put("description", fieldDesc);
                return;
            }
        }

        for (TypeGraphVertex ancestor : tgv.getAncestors()) {
            putFieldDescription(field, port, ancestor);
        }
    }

    private JSONArray enrichProperties(String operatorClass, JSONArray properties) throws JSONException {
        JSONArray result = new JSONArray();
        for (int i = 0; i < properties.length(); i++) {
            JSONObject propJ = properties.getJSONObject(i);
            String propName = WordUtils.capitalize(propJ.getString("name"));
            String getPrefix = (propJ.getString("type").equals("boolean")
                    || propJ.getString("type").equals("java.lang.Boolean")) ? "is" : "get";
            String setPrefix = "set";
            OperatorClassInfo oci = getOperatorClassWithGetterSetter(operatorClass, setPrefix + propName,
                    getPrefix + propName);
            if (oci == null) {
                result.put(propJ);
                continue;
            }
            MethodInfo setterInfo = oci.setMethods.get(setPrefix + propName);
            MethodInfo getterInfo = oci.getMethods.get(getPrefix + propName);

            if ((getterInfo != null && getterInfo.omitFromUI) || (setterInfo != null && setterInfo.omitFromUI)) {
                continue;
            }
            if (setterInfo != null) {
                addTagsToProperties(setterInfo, propJ);
            } else if (getterInfo != null) {
                addTagsToProperties(getterInfo, propJ);
            }
            result.put(propJ);
        }
        return result;
    }

    private OperatorClassInfo getOperatorClassWithGetterSetter(String operatorClass, String setterName,
            String getterName) {
        TypeGraphVertex tgv = typeGraph.getTypeGraphVertex(operatorClass);
        return getOperatorClassWithGetterSetter(tgv, setterName, getterName);
    }

    private OperatorClassInfo getOperatorClassWithGetterSetter(TypeGraphVertex tgv, String setterName,
            String getterName) {
        OperatorClassInfo oci = classInfo.get(tgv.typeName);
        if (oci != null && (oci.getMethods.containsKey(getterName) || oci.setMethods.containsKey(setterName))) {
            return oci;
        } else {
            if (tgv.getAncestors() != null) {
                for (TypeGraphVertex ancestor : tgv.getAncestors()) {
                    return getOperatorClassWithGetterSetter(ancestor, setterName, getterName);
                }
            }
        }

        return null;
    }

    private void addTagsToProperties(MethodInfo mi, JSONObject propJ) throws JSONException {
        //create description object. description tag enables the visual tools to display description of keys/values
        //of a map property, items of a list, properties within a complex type.
        JSONObject descriptionObj = new JSONObject();
        if (mi.comment != null) {
            descriptionObj.put("$", mi.comment);
        }
        for (Map.Entry<String, String> descEntry : mi.descriptions.entrySet()) {
            descriptionObj.put(descEntry.getKey(), descEntry.getValue());
        }
        if (descriptionObj.length() > 0) {
            propJ.put("descriptions", descriptionObj);
        }

        //create useSchema object. useSchema tag is added to enable visual tools to be able to render a text field
        //as a dropdown with choices populated from the schema attached to the port.
        JSONObject useSchemaObj = new JSONObject();
        for (Map.Entry<String, String> useSchemaEntry : mi.useSchemas.entrySet()) {
            useSchemaObj.put(useSchemaEntry.getKey(), useSchemaEntry.getValue());
        }
        if (useSchemaObj.length() > 0) {
            propJ.put("useSchema", useSchemaObj);
        }
    }

    public JSONObject describeClass(String clazzName) throws Exception {
        return describeClassByASM(clazzName);
    }

    public JSONObject describeClassByASM(String clazzName) throws Exception {
        return typeGraph.describeClass(clazzName);
    }

    public JSONObject describeClass(Class<?> clazz) throws Exception {
        JSONObject desc = new JSONObject();
        desc.put("name", clazz.getName());
        if (clazz.isEnum()) {
            @SuppressWarnings("unchecked")
            Class<Enum<?>> enumClass = (Class<Enum<?>>) clazz;
            ArrayList<String> enumNames = Lists.newArrayList();
            for (Enum<?> e : enumClass.getEnumConstants()) {
                enumNames.add(e.name());
            }
            desc.put("enum", enumNames);
        }
        UI_TYPE ui_type = UI_TYPE.getEnumFor(clazz);
        if (ui_type != null) {
            desc.put("uiType", ui_type.getName());
        }
        desc.put("properties", getClassProperties(clazz, 0));
        return desc;
    }

    private static String getDocName(String clazz) {
        return clazz.replace('.', '/').replace('$', '.') + ".html";
    }

    private JSONArray getClassProperties(Class<?> clazz, int level) throws IntrospectionException {
        JSONArray arr = new JSONArray();
        TypeDiscoverer td = new TypeDiscoverer();
        try {
            for (PropertyDescriptor pd : Introspector.getBeanInfo(clazz).getPropertyDescriptors()) {
                Method readMethod = pd.getReadMethod();
                if (readMethod != null) {
                    if (readMethod.getDeclaringClass() == java.lang.Enum.class) {
                        // skip getDeclaringClass
                        continue;
                    } else if ("class".equals(pd.getName())) {
                        // skip getClass
                        continue;
                    }
                } else {
                    // yields com.datatorrent.api.Context on JDK6 and com.datatorrent.api.Context.OperatorContext with JDK7
                    if ("up".equals(pd.getName())
                            && com.datatorrent.api.Context.class.isAssignableFrom(pd.getPropertyType())) {
                        continue;
                    }
                }
                //LOG.info("name: " + pd.getName() + " type: " + pd.getPropertyType());

                Class<?> propertyType = pd.getPropertyType();
                if (propertyType != null) {
                    JSONObject propertyObj = new JSONObject();
                    propertyObj.put("name", pd.getName());
                    propertyObj.put("canGet", readMethod != null);
                    propertyObj.put("canSet", pd.getWriteMethod() != null);
                    if (readMethod != null) {
                        for (Class<?> c = clazz; c != null; c = c.getSuperclass()) {
                            OperatorClassInfo oci = classInfo.get(c.getName());
                            if (oci != null) {
                                MethodInfo getMethodInfo = oci.getMethods.get(readMethod.getName());
                                if (getMethodInfo != null) {
                                    addTagsToProperties(getMethodInfo, propertyObj);
                                    break;
                                }
                            }
                        }
                        // type can be a type symbol or parameterized type
                        td.setTypeArguments(clazz, readMethod.getGenericReturnType(), propertyObj);
                    } else {
                        if (pd.getWriteMethod() != null) {
                            td.setTypeArguments(clazz, pd.getWriteMethod().getGenericParameterTypes()[0],
                                    propertyObj);
                        }
                    }
                    //if (!propertyType.isPrimitive() && !propertyType.isEnum() && !propertyType.isArray() && !propertyType.getName().startsWith("java.lang") && level < MAX_PROPERTY_LEVELS) {
                    //  propertyObj.put("properties", getClassProperties(propertyType, level + 1));
                    //}
                    arr.put(propertyObj);
                }
            }
        } catch (JSONException ex) {
            throw new RuntimeException(ex);
        }
        return arr;
    }

    private static final Pattern CAPS = Pattern.compile("([A-Z\\d][^A-Z\\d]*)");

    private static String decamelizeClassName(String className) {
        Matcher match = CAPS.matcher(className);
        StringBuilder deCameled = new StringBuilder();
        while (match.find()) {
            if (deCameled.length() == 0) {
                deCameled.append(match.group());
            } else {
                deCameled.append(" ");
                deCameled.append(match.group().toLowerCase());
            }
        }
        return deCameled.toString();
    }

    /**
     * Enrich portClassHier with class/interface names that map to a list of parent classes/interfaces.
     * For any class encountered, find its parents too.<br/>
     * Also find the port types which have assignable schema classes.
     *
     * @param oper                       Operator to work on
     * @param portClassHierarchy         In-Out param that contains a mapping of class/interface to its parents
     * @param portTypesWithSchemaClasses Json that will contain all the ports which have any schema classes.
     */
    public void buildAdditionalPortInfo(JSONObject oper, JSONObject portClassHierarchy,
            JSONObject portTypesWithSchemaClasses) {
        try {
            JSONArray ports = oper.getJSONArray(OperatorDiscoverer.PORT_TYPE_INFO_KEY);
            for (int i = 0; i < ports.length(); i++) {
                JSONObject port = ports.getJSONObject(i);

                String portType = port.optString("type");
                if (portType == null) {
                    //skipping if port type is null
                    continue;
                }

                if (typeGraph.size() == 0) {
                    buildTypeGraph();
                }

                try {
                    //building port class hierarchy
                    LinkedList<String> queue = Lists.newLinkedList();
                    queue.add(portType);
                    while (!queue.isEmpty()) {
                        String currentType = queue.remove();
                        if (portClassHierarchy.has(currentType)) {
                            //already present in the json so we skip.
                            continue;
                        }
                        List<String> immediateParents = typeGraph.getParents(currentType);
                        if (immediateParents == null) {
                            portClassHierarchy.put(currentType, Lists.<String>newArrayList());
                            continue;
                        }
                        portClassHierarchy.put(currentType, immediateParents);
                        queue.addAll(immediateParents);
                    }
                } catch (JSONException e) {
                    LOG.warn("building port type hierarchy {}", portType, e);
                }

                //finding port types with schema classes
                if (portTypesWithSchemaClasses.has(portType)) {
                    //already present in the json so skipping
                    continue;
                }
                if (portType.equals("byte") || portType.equals("short") || portType.equals("char")
                        || portType.equals("int") || portType.equals("long") || portType.equals("float")
                        || portType.equals("double") || portType.equals("java.lang.String")
                        || portType.equals("java.lang.Object")) {
                    //ignoring primitives, strings and object types as this information is needed only for complex types.
                    continue;
                }
                if (port.has("typeArgs")) {
                    //ignoring any type with generics
                    continue;
                }
                boolean hasSchemaClasses = false;
                List<String> instantiableDescendants = typeGraph.getInstantiableDescendants(portType);
                if (instantiableDescendants != null) {
                    for (String descendant : instantiableDescendants) {
                        try {
                            if (typeGraph.isInstantiableBean(descendant)) {
                                hasSchemaClasses = true;
                                break;
                            }
                        } catch (JSONException ex) {
                            LOG.warn("checking descendant is instantiable {}", descendant);
                        }
                    }
                }
                portTypesWithSchemaClasses.put(portType, hasSchemaClasses);
            }
        } catch (JSONException e) {
            // should not reach this
            LOG.error("JSON Exception {}", e);
            throw new RuntimeException(e);
        }
    }

    public JSONArray getDescendants(String fullClassName) {
        if (typeGraph.size() == 0) {
            buildTypeGraph();
        }
        return new JSONArray(typeGraph.getDescendants(fullClassName));
    }

    public TypeGraph getTypeGraph() {
        return typeGraph;
    }

}