mx.itesm.web2mexadl.cluster.ClusterAnalyzer.java Source code

Java tutorial

Introduction

Here is the source code for mx.itesm.web2mexadl.cluster.ClusterAnalyzer.java

Source

/*
 * Copyright 2011 jccastrejon
 *  
 * This file is part of Web2MexADL.
 *
 * Web2MexADL is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * any later version.
 *
 * Web2MexADL is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
    
 * You should have received a copy of the GNU General Public License
 * along with Web2MexADL.  If not, see <http://www.gnu.org/licenses/>.
 */
package mx.itesm.web2mexadl.cluster;

import java.awt.Color;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;

import mx.itesm.web2mexadl.dependencies.ClassDependencies;
import mx.itesm.web2mexadl.dependencies.DependenciesUtil;
import mx.itesm.web2mexadl.dependencies.DependencyAnalyzer;
import mx.itesm.web2mexadl.mvc.MvcAnalyzer;
import mx.itesm.web2mexadl.mvc.MvcDependencyCommand;
import mx.itesm.web2mexadl.util.Util;
import net.sf.javaml.clustering.Clusterer;
import net.sf.javaml.core.Dataset;
import net.sf.javaml.core.DefaultDataset;
import net.sf.javaml.core.DenseInstance;
import net.sf.javaml.tools.weka.WekaClusterer;

import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.jdom.Document;
import org.jdom.Element;
import org.jdom.JDOMException;
import org.jdom.Namespace;
import org.jdom.input.SAXBuilder;
import org.jdom.output.XMLOutputter;
import org.jdom.xpath.XPath;

import weka.clusterers.EM;

/**
 * The ClusterAnalyzer class is responsible for the identification of the
 * Clusters in which a web application is composed, and the generation of both a
 * MexADL document and a SVG file representing the software architecture
 * associated to the application.
 * 
 * @author jccastrejon
 * 
 */
public class ClusterAnalyzer {

    /**
     * Class logger.
     */
    private static Logger logger = Logger.getLogger(ClusterAnalyzer.class.getName());

    /**
     * xADL Types namespace.
     */
    public static final Namespace XADL_TYPES_NAMESPACE = Namespace.getNamespace("types",
            "http://www.ics.uci.edu/pub/arch/xArch/types.xsd");

    /**
     * Path to the links elements in a XADL document.
     */
    private static XPath linksPath;

    /**
     * Path to the components elements in a XADL document.
     */
    private static XPath componentsPath;

    /**
     * Path to the connectors elements in a XADL document.
     */
    private static XPath connectorsPath;

    /**
     * Path to the componentTypes elements in a XADL document.
     */
    private static XPath componentTypesPath;

    /**
     * SAX builder.
     */
    private static SAXBuilder saxBuilder = new SAXBuilder();

    static {
        try {
            ClusterAnalyzer.componentsPath = XPath
                    .newInstance("/instance:xArch/types:archStructure/types:component");
            ClusterAnalyzer.connectorsPath = XPath
                    .newInstance("/instance:xArch/types:archStructure/types:connector");
            ClusterAnalyzer.linksPath = XPath.newInstance("/instance:xArch/types:archStructure/types:link");
            ClusterAnalyzer.componentTypesPath = XPath
                    .newInstance("/instance:xArch/types:archTypes/types:componentType");
        } catch (Exception e) {
            ClusterAnalyzer.logger.log(Level.WARNING, "Error while xpath instances: ", e);
        }
    }

    /**
     * Classify each class within the specified path into one of the identified
     * Clusters of the application.
     * 
     * @param path
     *            Path to the directory containing the classes.
     * @param includeExternal
     *            Should the external dependencies be exported.
     * @param outputFile
     *            File where to export the classification results.
     * @return Map containing the classification results for each class.
     * @throws Exception
     *             If an Exception occurs during classification.
     */
    public static Map<String, Cluster> classifyClassesInDirectory(final File path, final boolean includeExternal,
            final File outputFile) throws Exception {
        Dataset[] clusters;
        Map<String, Cluster> returnValue;
        List<ClassDependencies> dependencies;
        Map<String, Set<String>> internalPackages;

        // Classify each class in the specified path
        dependencies = DependencyAnalyzer.getDirectoryDependencies(path.getAbsolutePath(),
                new MvcDependencyCommand());
        internalPackages = DependenciesUtil.getInternalPackages(dependencies,
                Util.getPropertyValues(Util.Variable.Type.getVariableName()));

        clusters = ClusterAnalyzer.generateClusters(dependencies);
        returnValue = ClusterAnalyzer.generateArchitecture(clusters, internalPackages, outputFile.getParentFile());

        if (outputFile != null) {
            DependenciesUtil.exportDependenciesToSVG(dependencies, includeExternal, outputFile, internalPackages,
                    new ClusterExportCommand(returnValue));
        }

        return returnValue;
    }

    /**
     * Classify each class within the specified WAR file into one of the
     * identified Clusters of the application.
     * 
     * @param file
     *            Path to the WAR file.
     * @param includeExternal
     *            Should the external dependencies be exported.
     * @param outputFile
     *            File where to export the classification results.
     * @return Map containing the classification results for each class.
     * @throws Exception
     *             If an Exception occurs during classification.
     */
    public static Map<String, Cluster> classifyClassesInWar(final File file, final boolean includeExternal,
            final File outputFile) throws Exception {
        Dataset[] clusters;
        Map<String, Cluster> returnValue;
        List<ClassDependencies> dependencies;
        Map<String, Set<String>> internalPackages;

        // Classify each class in the specified war
        dependencies = DependencyAnalyzer.getWarDependencies(file.getAbsolutePath(), new MvcDependencyCommand());

        // Remove the WEB-INF.classes prefix
        for (ClassDependencies dependency : dependencies) {
            dependency.setClassName(dependency.getClassName().replace("WEB-INF.classes.", ""));
            dependency.setPackageName(dependency.getPackageName().replace("WEB-INF.classes.", ""));
        }

        internalPackages = DependenciesUtil.getInternalPackages(dependencies,
                Util.getPropertyValues(Util.Variable.Type.getVariableName()));

        clusters = ClusterAnalyzer.generateClusters(dependencies);
        returnValue = ClusterAnalyzer.generateArchitecture(clusters, internalPackages, outputFile.getParentFile());

        if (outputFile != null) {
            DependenciesUtil.exportDependenciesToSVG(dependencies, includeExternal, outputFile, internalPackages,
                    new ClusterExportCommand(returnValue));
        }

        return returnValue;
    }

    /**
     * Generate a set of Clusters from the specified dependencies data.
     * 
     * @param dependencies
     * @return
     */
    private static Dataset[] generateClusters(final List<ClassDependencies> dependencies) {
        Dataset dataset;
        double[] values;
        boolean valueFound;
        Clusterer clusterer;
        String[] typeValues;
        int instanceTypeIndex;
        String[] suffixValues;
        Dataset[] returnValue;
        String[] externalApiValues;
        Map<String, String[]> externalApiPackages;

        // Valid suffixes to look for in the class names
        suffixValues = Util.getPropertyValues(Util.Variable.Suffix.getVariableName());

        // Valid file types to look for in the component names
        typeValues = Util.getPropertyValues(Util.Variable.Type.getVariableName());

        // Valid external api packages to look for in the classes dependencies
        externalApiValues = Util.getPropertyValues(Util.Variable.ExternalAPI.getVariableName());
        externalApiPackages = new HashMap<String, String[]>(externalApiValues.length);
        for (int i = 0; i < externalApiValues.length; i++) {
            if (!externalApiValues[i].equals("none")) {
                externalApiPackages.put(externalApiValues[i],
                        Util.getPropertyValues("externalApi." + externalApiValues[i] + ".packages"));
            }
        }

        // Get instances data
        returnValue = null;
        dataset = new DefaultDataset();
        values = new double[Util.Variable.values().length];
        for (ClassDependencies classDependencies : dependencies) {

            // Type
            instanceTypeIndex = 0;
            for (int i = 0; i < typeValues.length; i++) {
                String validType = typeValues[i];
                if (classDependencies.getClassName().endsWith("." + validType)) {
                    instanceTypeIndex = i;
                    break;
                }
            }
            values[Util.Variable.Type.ordinal()] = instanceTypeIndex;

            // ExternalAPI
            valueFound = false;
            externalApi: for (int i = 0; i < externalApiValues.length; i++) {
                String externalApi = externalApiValues[i];

                if (externalApi.equals("none")) {
                    continue;
                }

                // Check if any of the class' external dependencies match with
                // one of the key external dependencies
                if (classDependencies.getExternalDependencies() != null) {
                    for (String externalDependency : classDependencies.getExternalDependencies()) {
                        for (String externalPackage : externalApiPackages.get(externalApi)) {
                            if (externalDependency.toLowerCase().startsWith(externalPackage)) {
                                valueFound = true;
                                values[Util.Variable.ExternalAPI.ordinal()] = i;
                                break externalApi;
                            }
                        }
                    }
                }
            }

            // No key external dependency found
            if (!valueFound) {
                values[Util.Variable.ExternalAPI.ordinal()] = externalApiValues.length;
            }

            // Suffix
            valueFound = false;
            for (int i = 0; i < suffixValues.length; i++) {
                String suffix = suffixValues[i];
                if (classDependencies.getClassName().toLowerCase().endsWith(suffix)) {
                    valueFound = true;
                    values[Util.Variable.Suffix.ordinal()] = i;
                    break;
                }
            }

            // No key suffix found
            if (!valueFound) {
                values[Util.Variable.Suffix.ordinal()] = suffixValues.length;
            }

            // Save instance data
            dataset.add(new DenseInstance(values, classDependencies.getClassName()));
        }

        // Generate clusters
        clusterer = new WekaClusterer(new EM());
        returnValue = clusterer.cluster(dataset);

        return returnValue;
    }

    /**
     * Generate the architecture document associated to the specified web
     * application data.
     * 
     * @param clustersData
     * @param internalPackages
     * @param outputDir
     * @return
     * @throws JDOMException
     * @throws IOException
     */
    private static Map<String, Cluster> generateArchitecture(final Dataset[] clustersData,
            final Map<String, Set<String>> internalPackages, final File outputDir)
            throws IOException, JDOMException {
        int maxCount;
        int clusterIndex;
        int[] clustersCounts;
        Cluster[] clusters;
        List<Integer> clusterCountsList;
        Map<String, Cluster> returnValue;
        Set<String> currentPackageContent;
        StringBuilder[] implementationPackages;
        HashMap<String, Integer> clusterClasses;
        Map<String, Integer> packagesClassification;

        // Get the clusters assignments
        clusterIndex = -1;
        clusterClasses = new HashMap<String, Integer>();
        for (Dataset cluster : clustersData) {
            clusterIndex++;
            for (Object clazz : cluster.classes()) {
                clusterClasses.put(clazz.toString(), clusterIndex);
            }
        }

        // Initialize clusters
        clusters = new Cluster[clustersData.length];
        for (int i = 0; i < clustersData.length; i++) {
            clusters[i] = new Cluster(ClusterAnalyzer.getRandomColor());
        }

        // Initialize implementation packages
        implementationPackages = new StringBuilder[clustersData.length];
        for (int i = 0; i < implementationPackages.length; i++) {
            implementationPackages[i] = new StringBuilder();
        }

        // Classify packages
        clustersCounts = new int[clustersData.length];
        returnValue = new HashMap<String, Cluster>();
        packagesClassification = new HashMap<String, Integer>(internalPackages.size());
        for (String currentPackage : internalPackages.keySet()) {
            currentPackageContent = internalPackages.get(currentPackage);
            for (String component : currentPackageContent) {
                // Check if this component was assigned to any cluster
                if (clusterClasses.keySet().contains(component)) {
                    clustersCounts[clusterClasses.get(component)]++;
                    returnValue.put(component, clusters[clusterClasses.get(component)]);
                }
            }

            // Get the package cluster according to the most common cluster
            // between the package contents
            clusterCountsList = new ArrayList<Integer>(clustersCounts.length);
            for (int i : clustersCounts) {
                clusterCountsList.add(i);
            }
            maxCount = Collections.max(clusterCountsList);
            for (int i = 0; i < clustersCounts.length; i++) {
                if (clustersCounts[i] == maxCount) {
                    packagesClassification.put(currentPackage, i);
                    Util.addImplementationPackage(implementationPackages[i], currentPackage);
                    break;
                }
            }
        }

        ClusterAnalyzer.exportToMexADL(outputDir, implementationPackages);

        return returnValue;
    }

    /**
     * Export the given data into a MexADL architecture document.
     * 
     * @param outputDir
     * @param implementationPackages
     * @throws IOException
     * @throws JDOMException
     */
    @SuppressWarnings("unchecked")
    public static void exportToMexADL(final File outputDir, final StringBuilder... implementationPackages)
            throws IOException, JDOMException {
        File outputFile;
        Document document;
        String identifier;
        List<Element> links;
        String outputContents;
        XMLOutputter outputter;
        List<Element> components;
        List<Element> connectors;
        List<Element> componentTypes;
        List<Element> validComponents;
        StringBuilder implementationPackage;

        document = ClusterAnalyzer.saxBuilder
                .build(MvcAnalyzer.class.getResourceAsStream("/mx/itesm/web2mexadl/templates/ClusterTemplate.xml"));
        components = (List<Element>) ClusterAnalyzer.componentsPath.selectNodes(document);
        connectors = (List<Element>) ClusterAnalyzer.connectorsPath.selectNodes(document);
        links = (List<Element>) ClusterAnalyzer.linksPath.selectNodes(document);
        componentTypes = (List<Element>) ClusterAnalyzer.componentTypesPath.selectNodes(document);

        // Identify the clusters specified in the implementationPackages
        validComponents = new ArrayList<Element>(implementationPackages.length);
        for (int i = 0; i < implementationPackages.length; i++) {
            for (Element component : components) {
                if (component.getChild("description", ClusterAnalyzer.XADL_TYPES_NAMESPACE).getValue()
                        .equals("Cluster_" + i)) {
                    validComponents.add(component);
                }
            }
        }

        // Remove the unused clusters and their associated connectors and links
        for (Element component : components) {
            if (!validComponents.contains(component)) {
                identifier = component.getChild("description", ClusterAnalyzer.XADL_TYPES_NAMESPACE).getValue();
                identifier = identifier.substring(identifier.indexOf('_') + 1);

                // Remove component
                component.detach();

                // Remove component type
                for (Element type : componentTypes) {
                    if (type.getChild("description", ClusterAnalyzer.XADL_TYPES_NAMESPACE).getValue()
                            .equals("Cluster_" + identifier + "Type")) {
                        type.detach();
                        break;
                    }
                }

                // Remove connector
                for (Element connector : connectors) {
                    if (connector.getChild("description", ClusterAnalyzer.XADL_TYPES_NAMESPACE).getValue()
                            .equals("Connector_" + identifier)) {
                        connector.detach();
                        break;
                    }
                }

                // Remove links
                for (Element link : links) {
                    // Input link
                    if (link.getChild("description", ClusterAnalyzer.XADL_TYPES_NAMESPACE).getValue()
                            .equals("in" + identifier)) {
                        link.detach();
                    }

                    // Output from this component to other ones
                    else if (link.getChild("description", ClusterAnalyzer.XADL_TYPES_NAMESPACE).getValue()
                            .startsWith("out" + identifier)) {
                        link.detach();
                    }

                    // Output from other components to this one
                    else if (link.getChild("description", ClusterAnalyzer.XADL_TYPES_NAMESPACE).getValue()
                            .startsWith("out")
                            && link.getChild("description", ClusterAnalyzer.XADL_TYPES_NAMESPACE).getValue()
                                    .endsWith("-" + identifier)) {
                        link.detach();
                    }
                }

            }
        }

        // Write base architecture document
        outputter = new XMLOutputter();
        outputFile = new File(outputDir, "clusteredArchitecture.xml");
        FileUtils.deleteQuietly(outputFile);
        outputter.output(document, new FileWriter(outputFile));

        // Update implementation packages
        outputContents = FileUtils.readFileToString(outputFile, "UTF-8");
        for (int i = 0; i < implementationPackages.length; i++) {
            implementationPackage = implementationPackages[i];
            outputContents = StringUtils.replace(outputContents, "<!-- Cluster_" + i + " implementation -->",
                    implementationPackage.toString());
        }

        // Write final architecture document
        FileUtils.deleteQuietly(outputFile);
        FileUtils.write(outputFile, outputContents);
    }

    /**
     * Generate a random color, to identify Clusters. Based on:
     * http://stackoverflow.com/questions/43044/algorithm-to-randomly-generate
     * -an-aesthetically-pleasing-color-palette
     * 
     * @return
     */
    private static Color getRandomColor() {
        int red;
        int blue;
        int green;
        Random random;
        Color mixColor;
        Color returnValue;

        // Base color (white)
        mixColor = new Color(255, 255, 255);
        random = new Random();
        red = random.nextInt(256);
        green = random.nextInt(256);
        blue = random.nextInt(256);

        // Mix new color with base one
        red = (red + mixColor.getRed()) / 2;
        green = (green + mixColor.getGreen()) / 2;
        blue = (blue + mixColor.getBlue()) / 2;

        returnValue = new Color(red, green, blue);
        return returnValue;
    }
}