org.jboss.weld.logging.LogMessageIndexDiff.java Source code

Java tutorial

Introduction

Here is the source code for org.jboss.weld.logging.LogMessageIndexDiff.java

Source

/*
 * JBoss, Home of Professional Open Source
 * Copyright 2015, Red Hat, Inc., and individual contributors
 * by the @authors tag. See the copyright.txt in the distribution for a
 * full listing of individual contributors.
 *
 * 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 org.jboss.weld.logging;

import static org.jboss.weld.logging.Strings.ARTIFACT;
import static org.jboss.weld.logging.Strings.COLLISIONS;
import static org.jboss.weld.logging.Strings.DETECT_COLLISIONS_ONLY;
import static org.jboss.weld.logging.Strings.DIFFERENCES;
import static org.jboss.weld.logging.Strings.FILE_PATH;
import static org.jboss.weld.logging.Strings.ID;
import static org.jboss.weld.logging.Strings.INDEXES;
import static org.jboss.weld.logging.Strings.MESSAGE;
import static org.jboss.weld.logging.Strings.MESSAGES;
import static org.jboss.weld.logging.Strings.PROJECT_CODE;
import static org.jboss.weld.logging.Strings.SUPPRESSIONS;
import static org.jboss.weld.logging.Strings.SUPPRESS_WARNINGS_PREFIX;
import static org.jboss.weld.logging.Strings.TOTAL;
import static org.jboss.weld.logging.Strings.VALUE;
import static org.jboss.weld.logging.Strings.VERSION;

import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeMap;

import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonNull;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;

/**
 * Generates a diff file with the following JSON format:
 *
 * <pre>
 * {
 *  "indexes": [ { "version" : "3.0.0-SNAPSHOT", "artifact" : "org.jboss.weld:weld-core-impl", "filePath" : "/opt/source/file.json", "total" : 735 } , { "version" : "2.2.10.Final", "artifact" : "org.jboss.weld:weld-core-impl", "filePath" : "/opt/source/anotherFile.json", "total" : 745 } ],
 *  "detectCollisionsOnly": false,
 *  "total": 1,
 *  "differences": [
 *      {
 *          "id": 600,
 *          "messages": [
 *              {
 *                  "projectCode" : "WELD-",
 *                  "version" : "3.0.0-SNAPSHOT",
 *                  "value" : {
 *                      "method" : {
 *                          "sig" : "missingRetention(java.lang.Object param1)",
 *                          "retType":"void",
 *                          "interface":"org.jboss.weld.logging.ReflectionLogger"
 *                      },
 *                      "log" :{
 *                          "level" : "DEBUG"
 *                      },
 *                      "msg" : {
 *                          "id" : 600,
 *                          "value" : "{0} is missing...",
 *                          "format" : "MESSAGE_FORMAT"
 *                      }
 *                 }
 *              },
 *              {
 *                  "projectCode" : "WELD-",
 *                  "version" : "2.2.10.Final",
 *                  "value" : {
 *                      "method" : {
 *                          "sig" : "missingRetention(java.lang.Object param1)",
 *                          "retType":"void",
 *                          "interface":"org.jboss.weld.logging.ReflectionLogger"
 *                      },
 *                      "log" : {
 *                          "level" : "INFO"
 *                      },
 *                      "msg" : {
 *                          "id" : 600,
 *                          "value" : "{0} is missing...",
 *                          "format" : "MESSAGE_FORMAT"
 *                      }
 *                 }
 *              },
 *      }
 *  ]
 * }
 * </pre>
 *
 *
 * @author Martin Kouba
 */
public class LogMessageIndexDiff {

    /**
     *
     * @param args
     */
    public static void main(String[] args) {

        if (args.length == 0) {
            printUsage();
            return;
        }

        File outputFile = null;
        List<File> indexFiles = new ArrayList<File>();
        boolean detectCollisionsOnly = false;

        for (int i = 0; i < args.length; i++) {
            String arg = args[i];
            if ("-o".equals(arg)) {
                if (i >= args.length) {
                    throw new IllegalArgumentException("-o switch requires an output file name");
                }
                outputFile = new File(args[++i]);
            } else if ("-c".equals(arg)) {
                detectCollisionsOnly = true;
            } else {
                // Index file
                File file = new File(arg);
                if (!file.canRead()) {
                    throw new IllegalArgumentException("Unable to read the index file: " + file);
                }
                if (file.isFile()) {
                    indexFiles.add(file);
                } else if (file.isDirectory()) {
                    Collections.addAll(indexFiles, file.listFiles(new FileFilter() {
                        @Override
                        public boolean accept(File pathname) {
                            return pathname.isFile() && !pathname.isHidden();
                        }
                    }));
                }
            }
        }

        if (outputFile == null) {
            throw new IllegalStateException("The output file must be specified!");
        }

        LogMessageIndexDiff generator = new LogMessageIndexDiff();
        generator.createDiffFile(outputFile, generator.generate(indexFiles, detectCollisionsOnly));
    }

    private static void printUsage() {
        System.out.println("Usage: java -jar weld-logging-tools-shaded.jar [-c] -o file-name FILEORDIR...");
        System.out.println("Options:");
        System.out.println("  -c  detect only collisions");
        System.out.println("  -o  name the output diff file");
    }

    /**
     * Generates the JSON diff for the specified index files.
     *
     * @param indexFiles
     * @param outputFile
     * @param detectCollisionsOnly
     * @return
     */
    public JsonObject generate(List<File> indexFiles, boolean detectCollisionsOnly) {

        if (indexFiles.size() < 2) {
            throw new IllegalStateException("More than one index file must be specified: " + indexFiles);
        }

        // First parse the index files
        List<JsonObject> indexes = parseIndexFiles(indexFiles);

        // Build indexes metadata and check compared versions
        JsonArray indexesMeta = new JsonArray();
        List<String> indexesIds = new ArrayList<String>();
        Set<JsonElement> versions = new HashSet<JsonElement>();
        for (Iterator<JsonObject> iterator = indexes.iterator(); iterator.hasNext();) {
            JsonObject index = iterator.next();
            JsonElement version = index.get(VERSION);
            versions.add(version);
            String indexId = version.getAsString() + index.get(ARTIFACT).getAsString();
            if (indexesIds.contains(indexId)) {
                throw new IllegalStateException(
                        "Unable to compare index files with the same composite identifier (version and artifact id): "
                                + indexId);
            }
            indexesIds.add(indexId);
            JsonObject indexMeta = new JsonObject();
            indexMeta.add(VERSION, version);
            indexMeta.add(ARTIFACT, index.get(ARTIFACT));
            indexMeta.add(TOTAL, index.get(TOTAL));
            indexMeta.add(FILE_PATH, index.get(FILE_PATH));
            indexesMeta.add(indexMeta);
        }

        // Now let's find the differences
        // Note that messages don't need to have the ID specified (0) or may inherit the ID from another message with the same name (-1)
        JsonArray differences = findDifferences(versions.size(), detectCollisionsOnly, buildDataMap(indexes));

        JsonObject diff = new JsonObject();
        diff.add(INDEXES, indexesMeta);
        diff.add(DETECT_COLLISIONS_ONLY, Json.wrapPrimitive(detectCollisionsOnly));
        diff.add(TOTAL, Json.wrapPrimitive(differences.size()));
        diff.add(DIFFERENCES, differences.size() > 0 ? differences : JsonNull.INSTANCE);
        return diff;
    }

    /**
     *
     * @param outputFile
     * @param diff
     */
    public void createDiffFile(File outputFile, JsonObject diff) {
        try {
            Json.writeJsonElementToFile(diff, initOutputFile(outputFile));
        } catch (IOException e) {
            throw new IllegalStateException("Unable to write the diff file", e);
        }
    }

    private List<JsonObject> parseIndexFiles(List<File> indexFiles) {
        List<JsonObject> indexes = new ArrayList<JsonObject>();
        for (File indexFile : indexFiles) {
            try {
                JsonObject index = Json.readJsonElementFromFile(indexFile).getAsJsonObject();
                index.add(FILE_PATH, Json.wrapPrimitive(indexFile.toPath().toString()));
                indexes.add(index);
            } catch (IOException e) {
                throw new IllegalStateException("Unable to parse the index file: " + indexFile, e);
            }
        }
        // Sort indexes by version and artifact
        Collections.sort(indexes, new Comparator<JsonObject>() {
            @Override
            public int compare(JsonObject o1, JsonObject o2) {
                // Version and artifact must be always set
                int result = o1.get(VERSION).getAsString().compareTo(o2.get(VERSION).getAsString());
                return result == 0 ? o1.get(ARTIFACT).getAsString().compareTo(o2.get(ARTIFACT).getAsString())
                        : result;
            }
        });
        return indexes;
    }

    /**
     * @param indexes
     * @return a map of project codes to map of ids to map of versions to messages
     */
    private Map<String, Map<Integer, Map<String, List<JsonObject>>>> buildDataMap(List<JsonObject> indexes) {

        // Map message ID to the map of versions to messages
        // We use the TreeMap so that the keys are ordered
        Map<String, Map<Integer, Map<String, List<JsonObject>>>> dataMap = new HashMap<String, Map<Integer, Map<String, List<JsonObject>>>>();

        for (JsonObject index : indexes) {

            String version = index.get(VERSION).getAsString();

            for (JsonElement messageElement : index.get(MESSAGES).getAsJsonArray()) {

                JsonObject message = messageElement.getAsJsonObject();

                String projectCode = message.get(PROJECT_CODE).getAsString();
                Map<Integer, Map<String, List<JsonObject>>> idMap = dataMap.get(projectCode);
                if (idMap == null) {
                    idMap = new TreeMap<Integer, Map<String, List<JsonObject>>>();
                    dataMap.put(projectCode, idMap);
                }

                int id = message.get(MESSAGE).getAsJsonObject().get(ID).getAsInt();
                Map<String, List<JsonObject>> versionMap = idMap.get(id);
                List<JsonObject> messages = null;

                if (versionMap == null) {
                    versionMap = new HashMap<String, List<JsonObject>>();
                    idMap.put(id, versionMap);
                } else {
                    messages = versionMap.get(version);
                }
                if (messages == null) {
                    messages = new ArrayList<JsonObject>();
                    versionMap.put(version, messages);
                }
                messages.add(message);
            }
        }
        return dataMap;
    }

    private JsonArray findDifferences(int indexCount, boolean detectCollisionsOnly,
            Map<String, Map<Integer, Map<String, List<JsonObject>>>> dataMap) {
        JsonArray differences = new JsonArray();
        // Project code -> map of ids to...
        for (Entry<String, Map<Integer, Map<String, List<JsonObject>>>> entry : dataMap.entrySet()) {
            // ID -> map of versions to messages
            for (Entry<Integer, Map<String, List<JsonObject>>> idEntry : entry.getValue().entrySet()) {
                // For every ID attempt to find a difference for all messages with this id among all indexes
                if (detectCollisionsOnly) {
                    Set<String> collisions = getCollisions(idEntry.getValue());
                    if (!collisions.isEmpty()) {
                        JsonObject collision = new JsonObject();
                        collision.add(PROJECT_CODE, Json.wrapPrimitive(entry.getKey()));
                        collision.add(ID, Json.wrapPrimitive(idEntry.getKey()));
                        JsonArray messages = new JsonArray();
                        for (Entry<String, List<JsonObject>> versionEntry : idEntry.getValue().entrySet()) {
                            for (JsonObject message : versionEntry.getValue()) {
                                messages.add(wrap(versionEntry.getKey(), message));
                            }
                        }
                        collision.add(MESSAGES, messages);
                        collision.add(COLLISIONS, Json.arrayFromPrimitives(collisions));
                        differences.add(collision);
                    }
                } else if (isDifference(indexCount, idEntry.getValue())) {
                    JsonObject difference = new JsonObject();
                    difference.add(PROJECT_CODE, Json.wrapPrimitive(entry.getKey()));
                    difference.add(ID, Json.wrapPrimitive(idEntry.getKey()));
                    JsonArray messages = new JsonArray();
                    for (Entry<String, List<JsonObject>> versionEntry : idEntry.getValue().entrySet()) {
                        for (JsonObject message : versionEntry.getValue()) {
                            messages.add(wrap(versionEntry.getKey(), message));
                        }
                    }
                    difference.add(MESSAGES, messages);
                    differences.add(difference);
                }
            }
        }
        return differences;
    }

    private boolean isDifference(int indexCount, Map<String, List<JsonObject>> versionMap) {
        if (indexCount != versionMap.size()) {
            // The ID not found in all indexes
            return true;
        }
        List<List<JsonObject>> values = new ArrayList<List<JsonObject>>(versionMap.values());
        for (int i = 1; i < values.size(); i++) {
            List<JsonObject> current = values.get(i);
            List<JsonObject> previous = values.get(i - 1);
            if (current.size() != previous.size()) {
                // The ID not found in all versions
                return true;
            }
            if (current.size() == 1 && previous.size() == 1) {
                // Very often there will be only one element in the list
                return !areMessagesEqual(current.get(0), previous.get(0));
            }
            // There are several messages with the same ID
            // A diff is detected if the lists do not contain the same messages
            // At this point we can be sure the lists have the same size
            // Note that suppressions must be taken into account
            for (JsonObject previousMessage : previous) {
                if (!messageListContains(current, previousMessage)) {
                    return true;
                }
            }
        }
        return false;
    }

    private Set<String> getCollisions(Map<String, List<JsonObject>> versionMap) {
        List<List<JsonObject>> values = new ArrayList<List<JsonObject>>(versionMap.values());
        for (int i = 1; i < values.size(); i++) {
            List<JsonObject> current = values.get(i);
            List<JsonObject> previous = values.get(i - 1);
            if (current.size() == 1 && previous.size() == 1) {
                // Very often there will be only one element in the list
                return getCollisions(current.get(0), previous.get(0));
            }
            // TODO A collision is never detected if there are several messages with the same ID
        }
        return Collections.emptySet();
    }

    private boolean areMessagesEqual(JsonObject msg1, JsonObject msg2) {
        List<String> suppressions = extractSuppressions(msg1);
        suppressions.addAll(extractSuppressions(msg2));
        if (!suppressions.isEmpty()) {
            // Make a copy of JSON representations first
            JsonParser parser = new JsonParser();
            msg1 = parser.parse(msg1.toString()).getAsJsonObject();
            msg2 = parser.parse(msg2.toString()).getAsJsonObject();
            msg1.remove(SUPPRESSIONS);
            msg2.remove(SUPPRESSIONS);
            // Then remove all suppressed members
            // E.g. for @SuppressWarnings("weldlog:msg-value") we'd like to remove msgObj.msg.value
            for (String suppression : suppressions) {
                String[] suppressionParts = suppression.substring(SUPPRESS_WARNINGS_PREFIX.length()).split("-");
                removeSuppressedMember(msg1, suppressionParts);
                removeSuppressedMember(msg2, suppressionParts);
            }
        }
        return msg1.equals(msg2);
    }

    private Set<String> getCollisions(JsonObject msg1, JsonObject msg2) {
        List<String> suppressions = extractSuppressions(msg1);
        suppressions.addAll(extractSuppressions(msg2));
        if (!suppressions.isEmpty()) {
            // Make a copy of JSON representations first
            JsonParser parser = new JsonParser();
            msg1 = parser.parse(msg1.toString()).getAsJsonObject();
            msg2 = parser.parse(msg2.toString()).getAsJsonObject();
            msg1.remove(SUPPRESSIONS);
            msg2.remove(SUPPRESSIONS);
            // Then remove all suppressed members
            // E.g. for @SuppressWarnings("weldlog:msg-value") we'd like to remove msgObj.msg.value
            for (String suppression : suppressions) {
                String[] suppressionParts = suppression.substring(SUPPRESS_WARNINGS_PREFIX.length()).split("-");
                removeSuppressedMember(msg1, suppressionParts);
                removeSuppressedMember(msg2, suppressionParts);
            }
        }
        Set<String> collisions = new HashSet<>();
        for (Entry<String, JsonElement> entry : msg1.entrySet()) {
            JsonElement msg2Value = msg2.get(entry.getKey());
            if (!entry.getValue().equals(msg2Value)) {
                if (entry.getValue().isJsonObject() && msg2Value.isJsonObject()) {
                    Set<String> nestedCollisions = getCollisions(entry.getValue().getAsJsonObject(),
                            msg2Value.getAsJsonObject());
                    for (String nested : nestedCollisions) {
                        collisions.add(entry.getKey().toLowerCase() + "-" + nested);
                    }
                } else {
                    collisions.add(entry.getKey());
                }
            }
        }
        return collisions;
    }

    private void removeSuppressedMember(JsonObject msg, String[] suppressionParts) {
        JsonObject last = findLastJsonObject(msg, suppressionParts);
        if (last != null) {
            last.remove(suppressionParts[suppressionParts.length - 1]);
        }
    }

    private JsonObject findLastJsonObject(JsonObject jsonObject, String[] suppressionParts) {
        if (suppressionParts.length == 1) {
            return jsonObject;
        }
        JsonElement memberObject = jsonObject.get(suppressionParts[0]);
        if (memberObject != null && memberObject.isJsonObject()) {
            return findLastJsonObject(memberObject.getAsJsonObject(),
                    Arrays.copyOfRange(suppressionParts, 1, suppressionParts.length));
        }
        return null;
    }

    private boolean messageListContains(List<JsonObject> messages, JsonObject msg) {
        for (JsonObject element : messages) {
            if (areMessagesEqual(element, msg)) {
                return true;
            }
        }
        return false;
    }

    private List<String> extractSuppressions(JsonObject msg) {
        List<String> suppressionValues = new ArrayList<>();
        JsonElement suppressions = msg.get(SUPPRESSIONS);
        if (suppressions != null && suppressions.isJsonArray()) {
            for (JsonElement suppression : suppressions.getAsJsonArray()) {
                suppressionValues.add(suppression.getAsString());
            }
        }
        return suppressionValues;
    }

    private File initOutputFile(File outputFile) {
        if (!outputFile.exists()) {
            try {
                outputFile.createNewFile();
            } catch (IOException e) {
                throw new IllegalStateException("Unable to create the output file: " + outputFile);
            }
        }
        if (!outputFile.canWrite()) {
            throw new IllegalStateException("Unable to write to the output file: " + outputFile);
        }
        return outputFile;
    }

    private JsonObject wrap(String version, JsonElement element) {
        JsonObject versionAware = new JsonObject();
        versionAware.add(VERSION, Json.wrapPrimitive(version));
        versionAware.add(VALUE, element);
        return versionAware;
    }

}