com.github.juanmf.java2plant.render.PlantRenderer.java Source code

Java tutorial

Introduction

Here is the source code for com.github.juanmf.java2plant.render.PlantRenderer.java

Source

/*
 *  Copyright 2016 Juan Manuel Fernandez
 *
 *  Licensed 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.github.juanmf.java2plant.render;

import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import com.github.juanmf.java2plant.Parser;
import com.github.juanmf.java2plant.render.filters.Filter;
import com.github.juanmf.java2plant.render.filters.Filters;
import com.github.juanmf.java2plant.structure.Relation;
import com.github.juanmf.java2plant.structure.Use;
import com.github.juanmf.java2plant.util.SaveFileHelper;
import com.github.juanmf.java2plant.util.TypesHelper;

import com.google.common.eventbus.EventBus;
import edu.emory.mathcs.backport.java.util.Collections;
import org.apache.commons.lang3.StringUtils;

/**
 * @author juanmf@gmail.com
 */
public class PlantRenderer {

    private static final String TARGET_DIRECTORY = "target"; // default target directory is what maven uses

    private static final Map<Class<? extends Member>, MemberPrinter> memberPrinters = new HashMap<>();

    private final Set<Class<?>> types;
    private final Set<Relation> relations;

    private final Filter<Class<?>> classesFilter;
    private final Filter<Class<? extends Relation>> relationsTypeFilter;
    private final Filter<Relation> relationsFilter;

    private final NotesRenderer notesRenderer;
    private final String relevantPackages;

    static {
        MethodPrinter mp = new MethodPrinter();
        memberPrinters.put(Field.class, new FieldPrinter());
        memberPrinters.put(Constructor.class, mp);
        memberPrinters.put(Method.class, mp);
        EventBus eb = Parser.getEventBus();
        eb.register(new LollipopInterfaceListener());
    }

    public PlantRenderer(Set<Class<?>> types, Set<Relation> relations, String relevantPackages) {
        this(types, relations, relevantPackages, Filters.FILTER_CHAIN_RELATION_TYPE_STANDARD,
                Filters.FILTER_CHAIN_CLASSES_STANDARD, Filters.FILTER_CHAIN_RELATION_STANDARD);
    }

    public PlantRenderer(Set<Class<?>> types, Set<Relation> relations, String relevantPackages,
            Filter<Class<? extends Relation>> relationTypeFilter, Filter<Class<?>> classesFilter,
            Filter<Relation> relationsFilter) {
        this.types = types;
        this.relations = relations;
        this.relationsTypeFilter = relationTypeFilter;
        this.classesFilter = classesFilter;
        this.relationsFilter = relationsFilter;
        this.notesRenderer = new NotesRenderer();
        this.relevantPackages = relevantPackages;
    }

    /**
     * Render full contents
     *
     * <pre>
     *   * Classes
     *   * Relations
     * </pre>
     *
     * @return palntUML src code
     */
    public String render() {
        StringBuilder sb = new StringBuilder();
        sb.append("@startuml\n").append("' Created by juanmf@gmail.com\n\n")
                .append("' Using left to right direction to try a better layout feel free to edit\n")
                .append("left to right direction\n");

        sb.append("' Participants \n\n");
        addClasses(sb);

        sb.append("\n' Relations \n\n");
        addRelations(sb);

        sb.append("\n' Notes \n\n");
        addNotes(sb);

        sb.append("@enduml\n");

        // TODO: Lets decide if it is better to throw this exception so that we can log
        // with Maven (getLog()) to alert the User there was a problem creating
        // the PlantUML file
        try {
            SaveFileHelper.save(sb, TARGET_DIRECTORY);
        } catch (IOException e) {
            e.printStackTrace();
        }

        return sb.toString();
    }

    /**
     * Adds notes to the diagram src, so that it's easy to find relevant Classes in the resulting diagram
     *
     * @param sb
     */
    private void addNotes(StringBuilder sb) {
        for (String aClassOrPackage : TypesHelper.splitPackages(relevantPackages)) {
            if (null != TypesHelper.loadClass(aClassOrPackage, Parser.CLASS_LOADER)) {
                notesRenderer.render(sb, NotesRenderer.NOTE_RELEVANT_CLASS, aClassOrPackage);
            }
        }
    }

    /**
     * Basic Relations renderer, no filtering used.
     *
     * @param sb
     */
    protected void addRelations(StringBuilder sb) {
        ArrayList<Relation> relations = new ArrayList<Relation>(this.relations);
        sortRelations(relations);
        for (Relation r : relations) {
            if (!relationsTypeFilter.satisfy(r.getClass(), sb) || !relationsFilter.satisfy(r, sb)) {
                continue;
            }
            addRelation(sb, r);
        }
    }

    private void sortRelations(ArrayList<Relation> relations) {
        Collections.sort(relations, new Comparator<Relation>() {
            @Override
            public int compare(Relation o1, Relation o2) {
                int result = o1.getClass().equals(o2.getClass())
                        ? o1.getFromType().getName().compareTo(o1.getFromType().getName())
                        : o1.getClass().getName().compareTo(o2.getClass().getName());
                return result;
            }
        });
    }

    private void addRelation(StringBuilder sb, Relation r) {
        if (r instanceof Use && isToTypeInAggregations(r)) {
            return;
        }
        sb.append(r.toString()).append("\n");
    }

    private boolean isToTypeInAggregations(Relation r) {
        Class<?> toType = TypesHelper.loadClass(r.getToType(), Parser.CLASS_LOADER);
        Class<?> origin = r.getFromType();
        for (Field f : origin.getDeclaredFields()) {
            // TODO: There migth be cases where toType is a generic Type param and this won't do well e.g. Collection<Type>
            if (f.getType().equals(toType)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Basic Participants renderer, no filtering used.
     *
     * @param sb
     */
    protected void addClasses(StringBuilder sb) {
        for (Class<?> c : types) {
            if (!classesFilter.satisfy(c, sb)) {
                System.out.println("ClassFilter rejected class " + c);
                continue;
            }
            addClass(sb, c);
        }
    }

    protected void addClass(StringBuilder sb, Class<?> aClass) {
        String classDeclaration = aClass.isEnum() ? "enum " + aClass.getName() : aClass.toString();
        sb.append(classDeclaration);
        addClassTypeParams(sb, aClass);
        sb.append(" {\n");
        renderClassMembers(sb, aClass);
        sb.append("\n}\n");
    }

    private void addClassTypeParams(StringBuilder sb, Class<?> aClass) {
        List<String> typeParams = new ArrayList<>();
        // TODO: we are leaving lower bounds out, e.g. <? super Integer>
        for (TypeVariable t : aClass.getTypeParameters()) {
            Type[] bounds = t.getBounds();
            String jointBounds = TypesHelper.getSimpleName(StringUtils.join(bounds, "&"));
            typeParams.add(t.getName() + " extends " + jointBounds);
        }
        if (0 < typeParams.size()) {
            sb.append(" <").append(StringUtils.join(typeParams, ", ")).append(">");
        }
    }

    private void renderClassMembers(StringBuilder sb, Class<?> aClass) {
        List<String> fields = new ArrayList<>();
        List<String> methods = new ArrayList<>();
        List<String> constructors = new ArrayList<>();

        addMembers(aClass.getDeclaredFields(), fields);
        addMembers(aClass.getDeclaredConstructors(), constructors);
        addMembers(aClass.getDeclaredMethods(), methods);

        Collections.sort(fields);
        Collections.sort(methods);
        Collections.sort(constructors);

        for (String field : fields) {
            sb.append(field + "\n");
        }
        sb.append("--\n");
        for (String constructor : constructors) {
            sb.append(constructor + "\n");
        }
        for (String method : methods) {
            sb.append(method + "\n");
        }
    }

    private void addMembers(Member[] declaredMembers, List<String> plantMembers) {
        for (Member m : declaredMembers) {
            memberPrinters.get(m.getClass()).addMember(m, plantMembers);
        }
    }

    interface MemberPrinter {
        void addMember(Member m, List<String> plantMembers);
    }

    static class FieldPrinter implements MemberPrinter {
        @Override
        public void addMember(Member m, List<String> plantMembers) {
            Field f = (Field) m;
            if (f.isSynthetic()) {
                System.out.println("skiping synthetic" + f);
                return;
            }

            String msg = String.format("%s %s : %s", Modifiers.forModifier(f.getModifiers()), f.getName(),
                    TypesHelper.getSimpleName(f.getGenericType().toString()));
            plantMembers.add(msg);
        }
    }

    static class NullPrinter implements MemberPrinter {
        @Override
        public void addMember(Member m, List<String> plantMembers) {
            System.out.println(String.format("skipping member %s.", m));
        }
    }

    /**
     * Used for Constructors or Methods
     */
    static class MethodPrinter implements MemberPrinter {
        @Override
        public void addMember(Member m, List<String> plantMembers) {
            if (m.isSynthetic()) {
                System.out.println("skiping synthetic" + m);
                return;
            }
            String name = TypesHelper.getSimpleName(m.getName());
            String modif = Modifiers.forModifier(m.getModifiers()).toString();
            String returnType = (m instanceof Method)
                    ? " : " + TypesHelper.getSimpleName(((Method) m).getReturnType().getName())
                    : "";
            String params = buildParams(m);
            String msg = String.format("%s %s(%s) %s", modif, name, params, returnType);
            plantMembers.add(msg);
        }

        private String buildParams(Member m) {
            StringBuilder params = new StringBuilder();
            Type[] paramClasses = m instanceof Method ? ((Method) m).getGenericParameterTypes()
                    : ((Constructor<?>) m).getGenericParameterTypes();
            Iterator<? extends Type> it = Arrays.asList(paramClasses).iterator();
            while (it.hasNext()) {
                Type c = it.next();
                params.append(TypesHelper.getSimpleName(c.toString()));
                if (it.hasNext()) {
                    params.append(", ");
                }
            }
            return params.toString();
        }
    }

    private enum Modifiers {
        PUBLIC("+"), PROTECTED("#"), PRIVATE("-"), DEFAULT("~");

        String prefix;

        Modifiers(String prefix) {
            this.prefix = prefix;
        }

        public static Modifiers forModifier(int memberModifier) {
            Modifiers m = null;
            if (Modifier.isPrivate(memberModifier)) {
                m = PRIVATE;
            }
            if (Modifier.isProtected(memberModifier)) {
                m = PROTECTED;
            }
            if (Modifier.isPublic(memberModifier)) {
                m = PUBLIC;
            }
            if (null == m) {
                m = DEFAULT;
            }
            return m;
        }

        @Override
        public String toString() {
            return prefix + " ";
        }
    }
}