formatasm.FormatASM.java Source code

Java tutorial

Introduction

Here is the source code for formatasm.FormatASM.java

Source

package formatasm;

/**
 * Copyright 2015 Jason Aaron Osgood 
 * 
 * All rights reserved.
 * 
 * I'll pick a friendly open source license shortly. Probably BSD.
 * 
 **/

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

/**
 * References:
 *
 *    http://docs.oracle.com/javase/7/docs/api/java/util/Formatter.html
 *    https://en.wikipedia.org/wiki/Printf_format_string#Format_placeholders
 *
 */

public abstract class FormatASM extends DateTimeFormatter implements Opcodes {
    public static String printf(String template, Object... args) {

        FormatASM formatter;

        synchronized (lock) {
            if (map.containsKey(template)) {
                formatter = map.get(template);
            } else {
                String klazz = FormatASM.class.getName();
                String subklazz = klazz + "_" + id++;

                AccessClassLoader loader = AccessClassLoader.get(FormatASM.class);

                List<Spec> specs = parse(template, args);
                byte[] data = generateBytes(klazz, subklazz, specs);
                Class<FormatASM> accessClass = (Class<FormatASM>) loader.defineClass(subklazz, data);

                try {
                    formatter = accessClass.newInstance();
                    formatter.template = template;
                    formatter.specs = specs;
                    map.put(template, formatter);

                } catch (Throwable t) {
                    throw new RuntimeException("Error constructing FormatASM subclass: " + subklazz, t);
                }
            }
        }

        String result = formatter.format(args);
        return result;
    }

    public abstract String format(Object... args);

    //
    // %[argument_index$][flags][width][.precision][datetime|conversion]
    //
    // This regex attempts to balance correctness, concision, expression complexity, ease of
    // value extraction.
    //
    // Regex's groups (for extraction):
    //
    // 0:whole ( 1:index 2:flags 3:width 4:precision 5:notused ( 6:datetime 7:conv ))

    private static final String formatSpecifier = "%(\\d+\\$)?([-#+ 0,(\\<]*)?(\\d+)?(\\.\\d+)?(([tT][AaBbCcDdeFHhIjkLlMmNpQRrSsTYyZz])|([aAbBcCdeEfgGhHonsSxX%]))";

    private static Pattern pattern = Pattern.compile(formatSpecifier);

    public static List<Spec> parse(String input, Object... args) {
        ArrayList<Spec> specs = new ArrayList<Spec>();

        Matcher m = pattern.matcher(input);

        int autoIndex = 0;
        int i = 0;

        Spec prev = null;

        while (m.find()) {

            String whole = m.group(0);
            //         System.out.printf( "whole %s\n", whole);
            //         for( int n = 0; n < m.groupCount()+1; n++ )
            //         {
            //            System.out.printf( "group %d value %s\n", n, m.group( n ));
            //         }

            if (m.start() > i) {
                String text = input.substring(i, m.start());
                Spec spec = new Spec(i, m.start(), text);
                specs.add(spec);
            }

            // Remember varargs index for next iteration
            i = m.end();

            Spec spec = new Spec();
            specs.add(spec);

            if ("%%".equals(whole)) {
                spec.type = Type.PERCENT;
                continue;
            }

            String index = m.group(1);
            String flags = m.group(2);
            String width = m.group(3);
            String precision = m.group(4);
            String datetime = m.group(6);
            String conv = m.group(7);

            // Maybe assign values in second pass
            if (index == null) {
                spec.index = autoIndex;
                spec.value = args[autoIndex];
            } else {
                spec.index = Integer.valueOf(index);
                spec.value = args[spec.index];
            }
            autoIndex++;

            if (flags != null) {
                for (char c : flags.toCharArray()) {
                    switch (c) {
                    case '-':
                        spec.leftFlag = true;
                        break;
                    case '#':
                        spec.altFlag = true;
                        break;
                    case '+':
                        spec.plusFlag = true;
                        break;
                    case ' ':
                        spec.spaceFlag = true;
                        break;
                    case '0':
                        spec.zeroFlag = true;
                        break;
                    case ',':
                        spec.groupFlag = true;
                        break;
                    case '(':
                        spec.parensFlag = true;
                        break;

                    case '<':
                        spec.prevFlag = true;
                        spec.value = prev.value;
                        break;

                    default:
                        break;
                    }
                }
            }

            if (width != null) {
                spec.width = Integer.valueOf(width);
            }

            if (precision != null) {
                spec.precision = Integer.valueOf(precision);
            }

            if (datetime != null) {
                decode(spec, datetime);
            } else if (conv != null) {
                decode(spec, conv);
            }

            prev = spec;

        }

        if (i < input.length()) {
            String text = input.substring(i);
            Spec spec = new Spec(i, input.length(), text);
            specs.add(spec);
        }

        //      showme( specs, args );
        return specs;
    }

    //   public static String showme( List<Spec> specs, Object... args )
    //   {
    //      StringBuilder sb = new StringBuilder();
    //      for( Spec spec : specs ) {
    //         System.out.println( spec.value );
    //      }
    //      return sb.toString();
    //   }

    public static void decode(Spec spec, String conv) {
        char type = conv.charAt(0);

        switch (type) {
        case 'A':
            spec.upper = true;
        case 'a':
            spec.type = Type.FLOAT_HEX;
            break;

        case 'B':
            spec.upper = true;
        case 'b':
            spec.type = Type.BOOL;
            break;

        case 'C':
            spec.upper = true;
        case 'c':
            spec.type = Type.CHAR;
            break;

        case 'd':
            spec.type = Type.INT;
            break;

        case 'E':
            spec.upper = true;
        case 'e':
            spec.type = Type.SCIENTIFIC;
            break;

        case 'f':
            spec.type = Type.FLOAT;
            break;

        case 'G':
            spec.upper = true;
        case 'g':
            spec.type = Type.GENERAL;
            break;

        case 'H':
            spec.upper = true;
        case 'h':
            spec.type = Type.HASHCODE;
            break;

        case 'o':
            spec.type = Type.OCTAL;
            break;

        case 'S':
            spec.upper = true;
        case 's':
            spec.type = Type.STRING;
            break;

        case 'X':
            spec.upper = true;
        case 'x':
            spec.type = Type.INT_HEX;
            break;

        case 'T':
            spec.upper = true;
        case 't':
            spec.type = Type.DATETIME;
            spec.datetime = conv.charAt(1);
            break;

        default:
            break;
        }
    }

    public static byte[] generateBytes(String klazz, String subklazz, List<Spec> specs) {
        klazz = klazz.replace('.', '/');
        subklazz = subklazz.replace('.', '/');

        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
        MethodVisitor mv;

        cw.visit(V1_7, ACC_PUBLIC + ACC_SUPER, subklazz, null, klazz, null);

        {
            mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
            mv.visitCode();
            mv.visitVarInsn(ALOAD, 0);
            mv.visitMethodInsn(INVOKESPECIAL, klazz, "<init>", "()V");
            mv.visitInsn(RETURN);
            mv.visitMaxs(0, 0);
            mv.visitEnd();
        }

        {
            mv = cw.visitMethod(ACC_PUBLIC + ACC_VARARGS, "format", "([Ljava/lang/Object;)Ljava/lang/String;", null,
                    null);

            /// StringBuilder sb = new StringBuilder();
            mv.visitCode();
            mv.visitTypeInsn(NEW, "java/lang/StringBuilder");
            mv.visitInsn(DUP);
            mv.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V");
            mv.visitVarInsn(ASTORE, 2);

            for (Spec spec : specs) {
                switch (spec.type) {
                case CONTENT:
                    copyTemplateASM(klazz, mv, spec.begin, spec.end);
                    // Skip further processing
                    continue;

                case PERCENT:
                    mv.visitVarInsn(ALOAD, 2);
                    mv.visitIntInsn(BIPUSH, '%');
                    mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append",
                            "(C)Ljava/lang/StringBuilder;");
                    mv.visitInsn(POP);
                    // Skip further processing
                    continue;

                case INT:
                    formatIntegerASM(mv, spec);
                    break;

                case INT_HEX:
                    formatHexidecimalASM(mv, spec);
                    break;

                case OCTAL:
                    formatOctalASM(mv, spec);
                    break;

                case BOOL:
                case CHAR:
                    // text = args[?].toString();
                    mv.visitVarInsn(ALOAD, 1);
                    mv.visitIntInsn(BIPUSH, spec.index);
                    mv.visitInsn(AALOAD);
                    mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Object", "toString", "()Ljava/lang/String;");
                    mv.visitVarInsn(ASTORE, 3);
                    break;

                case STRING:
                    // text = (String) args[?]
                    mv.visitVarInsn(ALOAD, 1);
                    mv.visitIntInsn(BIPUSH, spec.index);
                    mv.visitInsn(AALOAD);
                    mv.visitTypeInsn(CHECKCAST, "java/lang/String");
                    mv.visitVarInsn(ASTORE, 3);
                    break;

                case DATETIME:
                    formatDateTimeASM(mv, spec);
                    break;

                default:
                    break;
                }

                if (spec.upper && spec.type != Type.DATETIME) {
                    toUpperCaseASM(mv);
                }

                if (spec.width > 0 && !spec.leftFlag) {
                    addWidthASM(mv, spec);
                }

                appendTextASM(mv);

                if (spec.width > 0 && spec.leftFlag) {
                    addWidthASM(mv, spec);
                }
            }

            /// return sb.toString();
            mv.visitVarInsn(ALOAD, 2);
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;");
            mv.visitInsn(ARETURN);

            mv.visitMaxs(0, 0);
            mv.visitEnd();
        }
        cw.visitEnd();

        return cw.toByteArray();
    }

    // / sb.append( template.substring( begin, end ));
    public static void copyTemplateASM(String klazz, MethodVisitor mv, int begin, int end) {
        mv.visitVarInsn(ALOAD, 2);
        mv.visitVarInsn(ALOAD, 0);
        mv.visitFieldInsn(GETFIELD, klazz, "template", "Ljava/lang/String;");
        mv.visitIntInsn(BIPUSH, begin);
        mv.visitIntInsn(BIPUSH, end);
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/String", "substring", "(II)Ljava/lang/String;");
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append",
                "(Ljava/lang/String;)Ljava/lang/StringBuilder;");
        mv.visitInsn(POP);
    }

    // text = formatInteger( (Integer) args[?], false, false, false, false );
    public static void formatIntegerASM(MethodVisitor mv, Spec spec) {
        mv.visitVarInsn(ALOAD, 1);
        mv.visitIntInsn(BIPUSH, spec.index);
        mv.visitInsn(AALOAD);
        mv.visitTypeInsn(CHECKCAST, "java/lang/Integer");
        mv.visitInsn(spec.parensFlag ? ICONST_1 : ICONST_0);
        mv.visitInsn(spec.groupFlag ? ICONST_1 : ICONST_0);
        mv.visitInsn(spec.plusFlag ? ICONST_1 : ICONST_0);
        mv.visitInsn(spec.spaceFlag ? ICONST_1 : ICONST_0);
        mv.visitMethodInsn(INVOKESTATIC, "formatasm/FormatASM", "formatInteger",
                "(Ljava/lang/Integer;ZZZZ)Ljava/lang/String;");
        mv.visitVarInsn(ASTORE, 3);
    }

    // text = formatDateTime( (Date) args[?], dateTimeChar );
    public static void formatDateTimeASM(MethodVisitor mv, Spec spec) {
        mv.visitVarInsn(ALOAD, 1);
        mv.visitIntInsn(BIPUSH, spec.index);
        mv.visitInsn(AALOAD);
        mv.visitInsn(spec.upper ? ICONST_1 : ICONST_0);
        mv.visitIntInsn(BIPUSH, spec.datetime);
        mv.visitMethodInsn(INVOKESTATIC, "formatasm/FormatASM", "formatDateTime",
                "(Ljava/lang/Object;ZC)Ljava/lang/String;");
        mv.visitVarInsn(ASTORE, 3);
    }

    // text = formatHexidecimal( (Integer) args[0] );
    public static void formatHexidecimalASM(MethodVisitor mv, Spec spec) {
        mv.visitVarInsn(ALOAD, 1);
        mv.visitIntInsn(BIPUSH, spec.index);
        mv.visitInsn(AALOAD);
        mv.visitTypeInsn(CHECKCAST, "java/lang/Integer");
        mv.visitMethodInsn(INVOKESTATIC, "formatasm/FormatASM", "formatHexidecimal",
                "(Ljava/lang/Integer;)Ljava/lang/String;");
        mv.visitVarInsn(ASTORE, 3);
    }

    // text = formatOctal( (Integer) args[0] );
    public static void formatOctalASM(MethodVisitor mv, Spec spec) {
        mv.visitVarInsn(ALOAD, 1);
        mv.visitIntInsn(BIPUSH, spec.index);
        mv.visitInsn(AALOAD);
        mv.visitTypeInsn(CHECKCAST, "java/lang/Integer");
        mv.visitMethodInsn(INVOKESTATIC, "formatasm/FormatASM", "formatOctal",
                "(Ljava/lang/Integer;)Ljava/lang/String;");
        mv.visitVarInsn(ASTORE, 3);
    }

    // sb.append( text );
    public static void appendTextASM(MethodVisitor mv) {
        mv.visitVarInsn(ALOAD, 2);
        mv.visitVarInsn(ALOAD, 3);
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append",
                "(Ljava/lang/String;)Ljava/lang/StringBuilder;");
        mv.visitInsn(POP);
    }

    // text = text.toUpperCase();
    public static void toUpperCaseASM(MethodVisitor mv) {
        mv.visitVarInsn(ALOAD, 3);
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/String", "toUpperCase", "()Ljava/lang/String;");
        mv.visitVarInsn(ASTORE, 3);
    }

    public static void addWidthASM(MethodVisitor mv, Spec spec) {
        mv.visitVarInsn(ALOAD, 2);
        mv.visitIntInsn(BIPUSH, spec.width);
        mv.visitVarInsn(ALOAD, 3);
        mv.visitMethodInsn(INVOKESTATIC, "formatasm/FormatASM", "addWidth",
                "(Ljava/lang/StringBuilder;ILjava/lang/String;)V");
    }

    // Convert integer to String, right-to-left
    public static String formatInteger(Integer ii, boolean parens, boolean comma, boolean plus, boolean space) {
        char[] buf = new char[32];
        int i = ii.intValue();
        int x = 31;
        boolean neg = false;
        if (i < 0) {
            neg = true;
            i = -i;
            if (parens)
                buf[x--] = ')';
        }

        byte nth = 0;
        do {
            if (comma && (nth++ % 3) == 0 && nth > 1) {
                buf[x--] = ',';
            }
            buf[x] = (char) ('0' + (i % 10));
            x--;
            i = i / 10;

        } while (i > 0);

        if (neg) {
            buf[x--] = parens ? '(' : '-';
        } else {
            if (plus) {
                buf[x--] = '+';
            } else if (space) {
                buf[x--] = ' ';
            }
        }
        String result = new String(buf, x + 1, 31 - x);
        return result;
    }

    public static String formatHexidecimal(Integer i) {
        return Integer.toHexString(i);
    }

    public static String formatOctal(Integer i) {
        return Integer.toOctalString(i);
    }

    public static void addWidth(StringBuilder sb, int width, String text) {
        for (int nth = width - text.length(); nth > 0; nth--) {
            sb.append(' ');
        }
    }

    protected String template;

    // Keep copy of parsed format specification for debugging
    protected List<Spec> specs;

    private static int id = 0;

    private static Object lock = new Object();

    private static HashMap<String, FormatASM> map = new HashMap<>();

}