com.jcwhatever.nucleus.localizer.LanguageGenerator.java Source code

Java tutorial

Introduction

Here is the source code for com.jcwhatever.nucleus.localizer.LanguageGenerator.java

Source

/* This file is part of NucleusLocalizer, licensed under the MIT License (MIT).
 *
 * Copyright (c) JCThePants (www.jcwhatever.com)
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

package com.jcwhatever.nucleus.localizer;

import org.objectweb.asm.ClassReader;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.tree.AnnotationNode;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.FieldNode;
import org.objectweb.asm.tree.MethodNode;

import java.io.Console;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Generates language key file from jar file.
 */
public class LanguageGenerator {

    private static final String LOCALIZABLE_CLASSPATH = "Lcom/jcwhatever/nucleus/managed/language/Localizable;";

    private static Pattern NEW_LINE = Pattern.compile("\n");

    private final File _jarFile;
    private final File _outputFile;
    private final String _version;

    /**
     * Constructor.
     *
     * @param jarFile     The jar file to parse for Localizable fields and annotations.
     * @param outputFile  The output key file.
     * @param version     The output file version.
     */
    public LanguageGenerator(File jarFile, File outputFile, String version) {

        _jarFile = jarFile;
        _outputFile = outputFile;
        _version = version;
    }

    /**
     * Generate key file.
     */
    public void generate() throws IOException {

        // check if file exists
        if (_outputFile.exists()) {

            Console console = System.console();
            if (console != null) {

                // find out if user wants to overwrite existing file.
                while (true) {
                    System.out.print("Language key file already exists. Overwrite? (Y or N)");

                    String input = console.readLine();

                    if (input.equalsIgnoreCase("Y")) {
                        break;
                    } else if (input.equalsIgnoreCase("N")) {
                        return;
                    }
                }
            }
        }

        System.out.println("Generating...");

        List<LiteralInfo> literals = getStringLiterals(_jarFile);
        Map<LiteralInfo, LiteralInfo> added = new HashMap<>(literals.size());

        if (literals.size() == 0) {
            System.out.println("No localizable string literals found. exiting.");
            return;
        }

        System.out.println(literals.size() + " literals found.");
        System.out.println("Opening file: " + _outputFile.getName());

        PrintWriter writer = new PrintWriter(_outputFile, "UTF-16");

        writer.write("version> ");
        writer.write(_version);
        writer.write("\n\n");

        for (int i = 0, j = 0; i < literals.size(); i++) {

            LiteralInfo info = literals.get(i);

            if (added.containsKey(info)) {

                LiteralInfo current = added.get(info);

                System.out.println("[DUPLICATE DETECTED] [SKIPPED]");
                System.out.println("    added: " + current.getComment());
                System.out.println("    duplicate: " + info.getComment());
                continue;
            }

            added.put(info, info);

            // write comment
            writer.write("# ");
            writer.write(info.getComment());
            writer.write('\n');

            // write string literal key
            Matcher matcher = NEW_LINE.matcher(info.getLiteral());
            writer.write(String.valueOf(j));
            writer.write("> ");
            writer.write(matcher.replaceAll("\\n"));
            writer.write('\n');
            writer.write('\n');
            j++;
        }

        writer.close();

        System.out.print("Finished.");
    }

    private boolean isLocalizableAnnotation(String annotationName) {
        return annotationName.equals(LOCALIZABLE_CLASSPATH);
    }

    // get localizable string literals from class files
    private List<LiteralInfo> getStringLiterals(File file) throws IOException {

        System.out.println("Opening jar file: " + file.getAbsolutePath());

        JarFile jarFile = new JarFile(file);
        Enumeration<JarEntry> entries = jarFile.entries();
        List<LiteralInfo> results = new ArrayList<>(50);

        Deque<ClassNode> parseQueue = new ArrayDeque<>(10);
        Map<String, AnnotationInfo> annotations = new HashMap<>(10);

        while (entries.hasMoreElements()) {
            JarEntry entry = entries.nextElement();

            if (entry.isDirectory() || !entry.getName().endsWith(".class"))
                continue;

            InputStream stream = jarFile.getInputStream(entry);
            ClassReader reader = new ClassReader(stream);

            ClassNode classNode = new ClassNode();
            reader.accept(classNode, 0);

            // check if class is an annotation
            if (!classNode.interfaces.isEmpty()
                    && classNode.interfaces.contains("java/lang/annotation/Annotation")) {

                // get annotation first so they can be applied in classes
                AnnotationInfo info = parseAnnotation(classNode);
                if (info == null)
                    continue;

                annotations.put(info.className, info);
            } else {
                // set aside non-annotation classes for now
                parseQueue.addLast(classNode);
            }

            stream.close();
        }

        // parse classes
        while (!parseQueue.isEmpty()) {
            ClassNode node = parseQueue.removeFirst();
            results.addAll(parseClass(node, annotations));
        }

        jarFile.close();

        return results;
    }

    // parse an annotation for localizable methods
    private AnnotationInfo parseAnnotation(ClassNode classNode) {

        @SuppressWarnings("unchecked")
        List<MethodNode> methods = (List<MethodNode>) classNode.methods;
        if (methods == null)
            return null;

        List<String> methodNames = new ArrayList<>(5);

        for (MethodNode node : methods) {

            @SuppressWarnings("unchecked")
            List<AnnotationNode> annotationNodes = node.visibleAnnotations;
            if (annotationNodes == null)
                continue;

            for (AnnotationNode annotation : annotationNodes) {
                if (!isLocalizableAnnotation(annotation.desc))
                    continue;

                System.out.println("@Localizable Annotation method found: " + node.name + ' ' + classNode.name);

                methodNames.add(node.name);
            }
        }

        return new AnnotationInfo(classNode.name, methodNames);
    }

    // parse a class for localizable fields and localizable annotation usages.
    private List<LiteralInfo> parseClass(ClassNode classNode, Map<String, AnnotationInfo> annotations) {

        List<LiteralInfo> result = new ArrayList<>(10);

        // parse class annotations
        @SuppressWarnings("unchecked")
        List<AnnotationNode> classAnnotations = classNode.visibleAnnotations;
        if (classAnnotations != null) {

            for (AnnotationNode node : classAnnotations) {

                String nodeName = node.desc.substring(1, node.desc.length() - 1);

                AnnotationInfo info = annotations.get(nodeName);
                if (info == null)
                    continue;

                result.addAll(parseAnnotationUsage(classNode, node));
            }
        }

        // parse fields
        @SuppressWarnings("unchecked")
        List<FieldNode> fields = (List<FieldNode>) classNode.fields;
        if (fields == null)
            return new ArrayList<>(0);

        for (FieldNode node : fields) {

            @SuppressWarnings("unchecked")
            List<AnnotationNode> annotationNodes = node.visibleAnnotations;
            if (annotationNodes == null)
                continue;

            for (AnnotationNode annotation : annotationNodes) {
                if (!isLocalizableAnnotation(annotation.desc))
                    continue;

                boolean isStatic = (node.access & Opcodes.ACC_STATIC) != 0;
                boolean isFinal = (node.access & Opcodes.ACC_FINAL) != 0;

                String desc = "FIELD: " + node.name + ' ' + classNode.name;

                if (!isStatic) {
                    System.out.println("[IGNORED] @Localizable field found but isn't static: " + desc);
                }

                if (!isFinal) {
                    System.out.println("[IGNORED] @Localizable field found but isn't final: " + desc);
                }

                if (!isStatic || !isFinal)
                    continue;

                if (node.value instanceof String) {
                    System.out.println("@Localizable field found: " + desc);

                    result.add(new LiteralInfo((String) node.value, desc));
                } else {
                    System.out.println(
                            "[IGNORED] @Localizable field found but did not contain a String value: " + desc);
                }
            }
        }

        return result;
    }

    // parse the usage of an annotation for the localizable text
    private List<LiteralInfo> parseAnnotationUsage(ClassNode classNode, AnnotationNode annotation) {

        if (annotation.values == null)
            return new ArrayList<>(0);

        List<LiteralInfo> result = new ArrayList<>(10);

        for (int i = 0; i < annotation.values.size(); i += 2) {

            String methodName = (String) annotation.values.get(i);
            Object valueObject = annotation.values.get(i + 1);

            List<String> values;
            boolean isArray;

            if (valueObject instanceof String) {
                values = new ArrayList<>(1);
                values.add((String) valueObject);
                isArray = false;
            } else if (valueObject instanceof List) {
                values = (List<String>) valueObject;
                isArray = true;
            } else {
                continue;
            }

            int count = 1;
            for (String value : values) {

                String nameSuffix = isArray ? "[" + count + '|' + values.size() + ']' : "()";

                String desc = "ANNOTATION METHOD: " + methodName + nameSuffix + ' ' + classNode.name + ' '
                        + annotation.desc;

                System.out.println("Annotation usage found: " + desc);
                result.add(new LiteralInfo(value, desc));
                count++;
            }
        }

        return result;
    }

    private static class AnnotationInfo {
        final String className;
        final List<String> methodNames;

        AnnotationInfo(String className, List<String> methodNames) {
            this.className = className;
            this.methodNames = methodNames;
        }
    }
}