eu.numberfour.n4js.antlr.compressor.ParserCompressorFragment.java Source code

Java tutorial

Introduction

Here is the source code for eu.numberfour.n4js.antlr.compressor.ParserCompressorFragment.java

Source

/**
 * Copyright (c) 2016 NumberFour AG.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *   NumberFour AG - Initial API and implementation
 */
package eu.numberfour.n4js.antlr.compressor;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.log4j.Logger;
import org.eclipse.xtext.generator.DefaultGeneratorFragment;

import com.google.common.base.Charsets;
import com.google.common.io.Files;

import eu.numberfour.n4js.antlr.compressor.IfElseCascade.Replacement;

/**
 * MWE fragment that injects the necessary code to compress the generated ANTLR V3 parser in order to avoid problems
 * with methods exceeding the 65535 bytes limit.
 */
public class ParserCompressorFragment extends DefaultGeneratorFragment {

    private final static Logger log = Logger.getLogger(ParserCompressorFragment.class);

    /**
     * Pattern for detecting const integer definitions. Package visible for testing.
     */
    final static Pattern CONST_DEF_PATTERN = Pattern
            .compile("^\\s*public\\ static\\ final\\ int\\ ([A-Za-z_0-9]+)=(-?[0-9]+);$", Pattern.MULTILINE);
    /**
     * Pattern for detecting state initializer which indicates the start of a if-else cascade. Package visible for
     * testing.
     */
    final static Pattern STATE_CHANGE_INITIALIZER_PATTERN = Pattern.compile("^\\s+s\\ =\\ -1;$", Pattern.MULTILINE);

    private final List<String> grammarFiles;
    private boolean backup = false;
    private int cascadeThreshold = 10;

    /**
     *
     */
    public ParserCompressorFragment() {
        grammarFiles = new ArrayList<>();
    }

    /**
     * Adds a grammar file name.
     */
    public void addGrammarFile(String fileName) {
        grammarFiles.add(fileName);
    }

    @Override
    public void generate(org.eclipse.xtext.Grammar grammar, org.eclipse.xpand2.XpandExecutionContext ctx) {
        for (String fileName : grammarFiles) {
            File file = new File(fileName);
            String javaSource = null;
            try {
                javaSource = Files.toString(file, Charsets.UTF_8);
            } catch (Exception ex) {
                log.error("Error reading file " + fileName + ": " + ex.getMessage());
            }
            if (javaSource != null) {
                String compressed = process(javaSource, file);
                log.info("File " + readableFileName(file) + " compressed: " + javaSource.length() + " --> "
                        + compressed.length() + " (" + 100 * compressed.length() / javaSource.length() + "%)");

                if (backup) {
                    try {
                        Files.copy(file, new File(file.getParentFile(), file.getName() + ".bak"));
                    } catch (IOException e) {
                        log.error("Error creating backup of " + readableFileName(file) + ": " + e.getMessage());
                        return;
                    }
                }

                try {
                    Files.write(compressed, file, Charsets.UTF_8);
                } catch (IOException e) {
                    log.error("Error writing compressed file " + readableFileName(file) + ": " + e.getMessage());
                }
            }
        }
    }

    String readableFileName(File f) {
        String path = f.getPath();
        int firstChar = 0;
        for (; firstChar < path.length(); firstChar++) {
            if (Character.isLetterOrDigit(path.charAt(firstChar))) {
                break;
            }
        }
        if (firstChar == path.length()) {
            firstChar = 0;
        }
        int firstSeg = path.indexOf(File.separatorChar, firstChar);
        if (firstSeg > 0) {
            return path.substring(firstChar, firstSeg) + "/.../" + f.getName();
        }
        return f.getName();
    }

    String process(String javaSource, File file) {

        Map<String, Integer> parserConstMap = createConstMap(javaSource);
        if (parserConstMap.isEmpty()) {
            log.info("No integer constants found in " + readableFileName(file));
            return null;
        }

        return processCascades(javaSource, parserConstMap);
    }

    String processCascades(String javaSource, Map<String, Integer> parserConstMap) {

        List<IfElseCascade> cascades = findCascades(javaSource);
        StringBuilder strb = new StringBuilder(javaSource.length());
        int offset = 0;
        int counter = 0;
        List<String> transMatrixDefs = new ArrayList<>(cascades.size());
        Map<String, String> arrayLiterals = new HashMap<>();
        for (IfElseCascade cascade : cascades) {
            if (cascade.size() >= cascadeThreshold) {
                strb.append(javaSource.substring(offset, cascade.start));
                Replacement replacements = cascade.getReplacements(parserConstMap, counter);

                String delegate = arrayLiterals.get(replacements.arrayLiteral);
                strb.append("\n                        ");
                if (delegate == null) {
                    transMatrixDefs.add(replacements.getMatrixDefinition());
                    arrayLiterals.put(replacements.arrayLiteral, IfElseCascade.getMatrixName(counter));
                    strb.append(replacements.getStatement());
                } else {
                    strb.append(replacements.getStatement(delegate));
                }
                strb.append("\n                        ");
                offset = cascade.end;
                counter++;
            }
        }
        strb.append(javaSource.substring(offset, javaSource.length()));

        strb.append("\nfinal class ").append(IfElseCascade.MATRIX_CLASS).append("{");
        for (String def : transMatrixDefs) {
            strb.append("\n\t").append(def);
        }
        strb.append("\n}");
        String compressed = strb.toString();
        return compressed;

    }

    private List<IfElseCascade> findCascades(String grammarContent) {
        Matcher matcher = STATE_CHANGE_INITIALIZER_PATTERN.matcher(grammarContent);
        IfElseCascadeParser ifElseCascadeParser = new IfElseCascadeParser(grammarContent);
        List<IfElseCascade> cascades = new ArrayList<>();
        int cascadeEnd = 0;
        while (matcher.find()) {
            int offset = matcher.end();
            if (offset < cascadeEnd) {
                throw new IllegalStateException("New state initializer in cascade found");
            }

            List<IfElseCascade> moreCascades = ifElseCascadeParser.findCascades(offset);
            if (moreCascades != null) {
                cascades.addAll(moreCascades);
            }
        }
        if (cascades.isEmpty()) {
            throw new IllegalStateException("No if-else cascades found.");
        }
        return cascades;
    }

    /**
     * Parses definition of token constants. Package visible for testing.
     */
    Map<String, Integer> createConstMap(String grammarContent) {
        Map<String, Integer> parserConstMap = new HashMap<>();
        Matcher matcher = CONST_DEF_PATTERN.matcher(grammarContent);
        while (matcher.find()) {
            String name = matcher.group(1);
            int value = Integer.parseInt(matcher.group(2));
            parserConstMap.put(name, value);
        }
        return parserConstMap;
    }

    /**
     * If backup is true, the original file is copied to *.bak". Default value is false.
     */
    public void setBackup(boolean backup) {
        this.backup = backup;
    }

    /**
     * The cascade threshold, if a cascade has less if/else statements, it is not replaced. Default value is 10. It is a
     * string due to limitations of MWE2.
     */
    public void setCascadeThreshold(String cascadeThreshold) {
        this.cascadeThreshold = Integer.parseInt(cascadeThreshold);
    }

}