com.github.sommeri.sourcemap.SourceMapConsumerV3.java Source code

Java tutorial

Introduction

Here is the source code for com.github.sommeri.sourcemap.SourceMapConsumerV3.java

Source

/*
 * Copyright 2011 The Closure Compiler Authors.
 *
 * 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 com.github.sommeri.sourcemap;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import com.github.sommeri.sourcemap.Base64VLQ.CharIterator;
import com.github.sommeri.sourcemap.Mapping.OriginalMapping;
import com.github.sommeri.sourcemap.Mapping.OriginalMapping.Builder;

/**
 * Class for parsing version 3 of the SourceMap format, as produced by the
 * Closure Compiler, etc.
 * http://code.google.com/p/closure-compiler/wiki/SourceMaps
 * 
 * @author johnlenz@google.com (John Lenz)
 */
public class SourceMapConsumerV3 implements SourceMapConsumer, SourceMappingReversable {
    static final int UNMAPPED = -1;

    //SMS: (source map separation):  added this
    private String file;
    private String sourceRoot;

    private String[] sources;
    private String[] sourcesContent;
    private String[] names;
    private int lineCount;
    // Slots in the lines list will be null if the line does not have any entries.
    private ArrayList<ArrayList<Entry>> lines = null;
    /** originalFile path ==> original line ==> target mappings */
    private Map<String, Map<Integer, Collection<OriginalMapping>>> reverseSourceMapping;

    public SourceMapConsumerV3() {

    }

    static class DefaultSourceMapSupplier implements SourceMapSupplier {
        @Override
        public String getSourceMap(String url) {
            return null;
        }
    }

    /**
     * Parses the given contents containing a source map.
     */
    @Override
    public void parse(String contents) throws SourceMapParseException {
        parse(contents, null);
    }

    /**
     * Parses the given contents containing a source map.
     */
    public void parse(String contents, SourceMapSupplier sectionSupplier) throws SourceMapParseException {
        try {
            JSONObject sourceMapRoot = new JSONObject(contents);
            parse(sourceMapRoot, sectionSupplier);
        } catch (JSONException ex) {
            throw new SourceMapParseException("JSON parse exception: " + ex);
        }
    }

    /**
     * Parses the given contents containing a source map.
     */
    public void parse(JSONObject sourceMapRoot) throws SourceMapParseException {
        parse(sourceMapRoot, null);
    }

    /**
     * Parses the given contents containing a source map.
     */
    public void parse(JSONObject sourceMapRoot, SourceMapSupplier sectionSupplier) throws SourceMapParseException {
        try {
            // Check basic assertions about the format.
            int version = sourceMapRoot.getInt("version");
            if (version != 3) {
                throw new SourceMapParseException("Unknown version: " + version);
            }

            this.file = sourceMapRoot.getString("file");
            if (file.isEmpty()) {
                //SMS: (source map separation):  commented this - I need more tolerant parser
                //throw new SourceMapParseException("File entry is missing or empty ");
            }
            if (sourceMapRoot.has("sourceRoot"))
                this.sourceRoot = sourceMapRoot.getString("sourceRoot");

            if (sourceMapRoot.has("sections")) {
                // Looks like a index map, try to parse it that way.
                parseMetaMap(sourceMapRoot, sectionSupplier);
                return;
            }

            lineCount = sourceMapRoot.getInt("lineCount");
            String lineMap = sourceMapRoot.getString("mappings");

            sources = getJavaStringArray(sourceMapRoot.getJSONArray("sources"));
            if (sourceMapRoot.has("sourcesContent")) {
                sourcesContent = getJavaStringArray(sourceMapRoot.getJSONArray("sourcesContent"));
            } else {
                sourcesContent = new String[sources.length];
            }
            names = getJavaStringArray(sourceMapRoot.getJSONArray("names"));

            lines = new ArrayList<ArrayList<Entry>>(lineCount);

            new MappingBuilder(lineMap).build();
        } catch (JSONException ex) {
            throw new SourceMapParseException("JSON parse exception: " + ex);
        }
    }

    /**
     * @param sourceMapRoot
     * @throws SourceMapParseException
     */
    private void parseMetaMap(JSONObject sourceMapRoot, SourceMapSupplier sectionSupplier)
            throws SourceMapParseException {
        if (sectionSupplier == null) {
            sectionSupplier = new DefaultSourceMapSupplier();
        }

        try {
            // Check basic assertions about the format.
            int version = sourceMapRoot.getInt("version");
            if (version != 3) {
                throw new SourceMapParseException("Unknown version: " + version);
            }

            String file = sourceMapRoot.getString("file");
            if (file.isEmpty()) {
                throw new SourceMapParseException("File entry is missing or empty");
            }

            if (sourceMapRoot.has("lineCount") || sourceMapRoot.has("mappings") || sourceMapRoot.has("sources")
                    || sourceMapRoot.has("names")) {
                throw new SourceMapParseException("Invalid map format");
            }

            SourceMapGeneratorV3 generator = new SourceMapGeneratorV3();
            JSONArray sections = sourceMapRoot.getJSONArray("sections");
            for (int i = 0, count = sections.length(); i < count; i++) {
                JSONObject section = sections.getJSONObject(i);
                if (section.has("map") && section.has("url")) {
                    throw new SourceMapParseException(
                            "Invalid map format: section may not have both 'map' and 'url'");
                }
                JSONObject offset = section.getJSONObject("offset");
                int line = offset.getInt("line");
                int column = offset.getInt("column");
                String mapSectionContents;
                if (section.has("url")) {
                    String url = section.getString("url");
                    mapSectionContents = sectionSupplier.getSourceMap(url);
                    if (mapSectionContents == null) {
                        throw new SourceMapParseException("Unable to retrieve: " + url);
                    }
                } else if (section.has("map")) {
                    mapSectionContents = section.getString("map");
                } else {
                    throw new SourceMapParseException(
                            "Invalid map format: section must have either 'map' or 'url'");
                }
                generator.mergeMapSection(line, column, mapSectionContents);
            }

            StringBuilder sb = new StringBuilder();
            try {
                generator.appendTo(sb, file);
            } catch (IOException e) {
                // Can't happen.
                throw new RuntimeException(e);
            }

            parse(sb.toString());
        } catch (IOException ex) {
            throw new SourceMapParseException("IO exception: " + ex);
        } catch (JSONException ex) {
            throw new SourceMapParseException("JSON parse exception: " + ex);
        }
    }

    @Override
    public OriginalMapping getMappingForLine(int lineNumber, int column) {
        // Normalize the line and column numbers to 0.
        lineNumber--;
        column--;

        if (lineNumber < 0 || lineNumber >= lines.size()) {
            return null;
        }

        Preconditions.checkState(lineNumber >= 0);
        Preconditions.checkState(column >= 0);

        // If the line is empty return the previous mapping.
        if (lines.get(lineNumber) == null) {
            return getPreviousMapping(lineNumber);
        }

        ArrayList<Entry> entries = lines.get(lineNumber);
        // No empty lists.
        Preconditions.checkState(entries.size() > 0);
        if (entries.get(0).getGeneratedColumn() > column) {
            return getPreviousMapping(lineNumber);
        }

        int index = search(entries, column, 0, entries.size() - 1);
        Preconditions.checkState(index >= 0, "unexpected: " + index);
        return getOriginalMappingForEntry(entries.get(index));
    }

    @Override
    public Collection<String> getOriginalSources() {
        return Arrays.asList(sources);
    }

    public Collection<String> getOriginalSourcesContent() {
        return Arrays.asList(sourcesContent);
    }

    public String getFile() {
        return file;
    }

    public String getSourceRoot() {
        return sourceRoot;
    }

    @Override
    public Collection<OriginalMapping> getReverseMapping(String originalFile, int line, int column) {
        // TODO(user): This implementation currently does not make use of the column
        // parameter.

        // Synchronization needs to be handled by callers.
        if (reverseSourceMapping == null) {
            createReverseMapping();
        }

        Map<Integer, Collection<OriginalMapping>> sourceLineToCollectionMap = reverseSourceMapping
                .get(originalFile);

        if (sourceLineToCollectionMap == null) {
            return Collections.emptyList();
        } else {
            Collection<OriginalMapping> mappings = sourceLineToCollectionMap.get(line);

            if (mappings == null) {
                return Collections.emptyList();
            } else {
                return mappings;
            }
        }
    }

    private String[] getJavaStringArray(JSONArray array) throws JSONException {
        int len = array.length();
        String[] result = new String[len];
        for (int i = 0; i < len; i++) {
            result[i] = array.isNull(i) ? null : array.getString(i);
        }
        return result;
    }

    private class MappingBuilder {
        private static final int MAX_ENTRY_VALUES = 5;
        private final StringCharIterator content;
        private int line = 0;
        private int previousCol = 0;
        private int previousSrcId = 0;
        private int previousSrcLine = 0;
        private int previousSrcColumn = 0;
        private int previousNameId = 0;

        MappingBuilder(String lineMap) {
            this.content = new StringCharIterator(lineMap);
        }

        void build() {
            int[] temp = new int[MAX_ENTRY_VALUES];
            ArrayList<Entry> entries = new ArrayList<Entry>();
            while (content.hasNext()) {
                // ';' denotes a new line.
                if (tryConsumeToken(';')) {
                    // The line is complete, store the result for the line,
                    // null if the line is empty.
                    ArrayList<Entry> result;
                    if (entries.size() > 0) {
                        result = entries;
                        // A new array list for the next line.
                        entries = new ArrayList<Entry>();
                    } else {
                        result = null;
                    }
                    lines.add(result);
                    entries.clear();
                    line++;
                    previousCol = 0;
                } else {
                    // grab the next entry for the current line.
                    int entryValues = 0;
                    while (!entryComplete()) {
                        temp[entryValues] = nextValue();
                        entryValues++;
                    }
                    Entry entry = decodeEntry(temp, entryValues);

                    validateEntry(entry);
                    entries.add(entry);

                    // Consume the separating token, if there is one.
                    tryConsumeToken(',');
                }
            }
        }

        /**
         * Sanity check the entry.
         */
        private void validateEntry(Entry entry) {
            Preconditions.checkState(line < lineCount);
            Preconditions
                    .checkState(entry.getSourceFileId() == UNMAPPED || entry.getSourceFileId() < sources.length);
            Preconditions.checkState(
                    entry.getSourceFileId() == UNMAPPED || entry.getSourceFileId() < sourcesContent.length);
            Preconditions.checkState(entry.getNameId() == UNMAPPED || entry.getNameId() < names.length);
        }

        /**
         * Decodes the next entry, using the previous encountered values to decode
         * the relative values.
         * 
         * @param vals
         *          An array of integers that represent values in the entry.
         * @param entryValues
         *          The number of entries in the array.
         * @return The entry object.
         */
        private Entry decodeEntry(int[] vals, int entryValues) {
            Entry entry;
            switch (entryValues) {
            // The first values, if present are in the following order:
            //   0: the starting column in the current line of the generated file
            //   1: the id of the original source file
            //   2: the starting line in the original source
            //   3: the starting column in the original source
            //   4: the id of the original symbol name
            // The values are relative to the last encountered value for that field.
            // Note: the previously column value for the generated file is reset
            // to '0' when a new line is encountered.  This is done in the 'build'
            // method.

            case 1:
                // An unmapped section of the generated file.
                entry = new UnmappedEntry(vals[0] + previousCol);
                // Set the values see for the next entry.
                previousCol = entry.getGeneratedColumn();
                return entry;

            case 4:
                // A mapped section of the generated file.
                entry = new UnnamedEntry(vals[0] + previousCol, vals[1] + previousSrcId, vals[2] + previousSrcLine,
                        vals[3] + previousSrcColumn);
                // Set the values see for the next entry.
                previousCol = entry.getGeneratedColumn();
                previousSrcId = entry.getSourceFileId();
                previousSrcLine = entry.getSourceLine();
                previousSrcColumn = entry.getSourceColumn();
                return entry;

            case 5:
                // A mapped section of the generated file, that has an associated
                // name.
                entry = new NamedEntry(vals[0] + previousCol, vals[1] + previousSrcId, vals[2] + previousSrcLine,
                        vals[3] + previousSrcColumn, vals[4] + previousNameId);
                // Set the values see for the next entry.
                previousCol = entry.getGeneratedColumn();
                previousSrcId = entry.getSourceFileId();
                previousSrcLine = entry.getSourceLine();
                previousSrcColumn = entry.getSourceColumn();
                previousNameId = entry.getNameId();
                return entry;

            default:
                throw new IllegalStateException("Unexpected number of values for entry:" + entryValues);
            }
        }

        private boolean tryConsumeToken(char token) {
            if (content.hasNext() && content.peek() == token) {
                // consume the comma
                content.next();
                return true;
            }
            return false;
        }

        private boolean entryComplete() {
            if (!content.hasNext()) {
                return true;
            }

            char c = content.peek();
            return (c == ';' || c == ',');
        }

        private int nextValue() {
            return Base64VLQ.decode(content);
        }
    }

    /**
     * Perform a binary search on the array to find a section that covers the
     * target column.
     */
    private int search(ArrayList<Entry> entries, int target, int start, int end) {
        while (true) {
            int mid = ((end - start) / 2) + start;
            int compare = compareEntry(entries, mid, target);
            if (compare == 0) {
                return mid;
            } else if (compare < 0) {
                // it is in the upper half
                start = mid + 1;
                if (start > end) {
                    return end;
                }
            } else {
                // it is in the lower half
                end = mid - 1;
                if (end < start) {
                    return end;
                }
            }
        }
    }

    /**
     * Compare an array entry's column value to the target column value.
     */
    private int compareEntry(ArrayList<Entry> entries, int entry, int target) {
        return entries.get(entry).getGeneratedColumn() - target;
    }

    /**
     * Returns the mapping entry that proceeds the supplied line or null if no
     * such entry exists.
     */
    private OriginalMapping getPreviousMapping(int lineNumber) {
        do {
            if (lineNumber == 0) {
                return null;
            }
            lineNumber--;
        } while (lines.get(lineNumber) == null);
        ArrayList<Entry> entries = lines.get(lineNumber);
        return getOriginalMappingForEntry(entries.get(entries.size() - 1));
    }

    /**
     * Creates an "OriginalMapping" object for the given entry object.
     */
    private OriginalMapping getOriginalMappingForEntry(Entry entry) {
        if (entry.getSourceFileId() == UNMAPPED) {
            return null;
        } else {
            // Adjust the line/column here to be start at 1.
            Builder x = OriginalMapping.newBuilder().setOriginalFile(sources[entry.getSourceFileId()])
                    .setLineNumber(entry.getSourceLine() + 1).setColumnPosition(entry.getSourceColumn() + 1);
            if (entry.getNameId() != UNMAPPED) {
                x.setIdentifier(names[entry.getNameId()]);
            }
            return x.build();
        }
    }

    /**
     * Reverse the source map; the created mapping will allow us to quickly go
     * from a source file and line number to a collection of target
     * OriginalMappings.
     */
    private void createReverseMapping() {
        reverseSourceMapping = new HashMap<String, Map<Integer, Collection<OriginalMapping>>>();

        for (int targetLine = 0; targetLine < lines.size(); targetLine++) {
            ArrayList<Entry> entries = lines.get(targetLine);

            if (entries != null) {
                for (Entry entry : entries) {
                    if (entry.getSourceFileId() != UNMAPPED && entry.getSourceLine() != UNMAPPED) {
                        String originalFile = sources[entry.getSourceFileId()];

                        if (!reverseSourceMapping.containsKey(originalFile)) {
                            reverseSourceMapping.put(originalFile,
                                    new HashMap<Integer, Collection<OriginalMapping>>());
                        }

                        Map<Integer, Collection<OriginalMapping>> lineToCollectionMap = reverseSourceMapping
                                .get(originalFile);

                        int sourceLine = entry.getSourceLine();

                        if (!lineToCollectionMap.containsKey(sourceLine)) {
                            lineToCollectionMap.put(sourceLine, new ArrayList<OriginalMapping>(1));
                        }

                        Collection<OriginalMapping> mappings = lineToCollectionMap.get(sourceLine);

                        Builder builder = OriginalMapping.newBuilder().setLineNumber(targetLine)
                                .setColumnPosition(entry.getGeneratedColumn());

                        mappings.add(builder.build());
                    }
                }
            }
        }
    }

    /**
     * A implementation of the Base64VLQ CharIterator used for decoding the
     * mappings encoded in the JSON string.
     */
    private static class StringCharIterator implements CharIterator {
        final String content;
        final int length;
        int current = 0;

        StringCharIterator(String content) {
            this.content = content;
            this.length = content.length();
        }

        @Override
        public char next() {
            return content.charAt(current++);
        }

        char peek() {
            return content.charAt(current);
        }

        @Override
        public boolean hasNext() {
            return current < length;
        }
    }

    /**
     * Represents a mapping entry in the source map.
     */
    private interface Entry {
        int getGeneratedColumn();

        int getSourceFileId();

        int getSourceLine();

        int getSourceColumn();

        int getNameId();
    }

    /**
     * This class represents a portion of the generated file, that is not mapped
     * to a section in the original source.
     */
    private static class UnmappedEntry implements Entry {
        private final int column;

        UnmappedEntry(int column) {
            this.column = column;
        }

        @Override
        public int getGeneratedColumn() {
            return column;
        }

        @Override
        public int getSourceFileId() {
            return UNMAPPED;
        }

        @Override
        public int getSourceLine() {
            return UNMAPPED;
        }

        @Override
        public int getSourceColumn() {
            return UNMAPPED;
        }

        @Override
        public int getNameId() {
            return UNMAPPED;
        }
    }

    /**
     * This class represents a portion of the generated file, that is mapped to a
     * section in the original source.
     */
    private static class UnnamedEntry extends UnmappedEntry {
        private final int srcFile;
        private final int srcLine;
        private final int srcColumn;

        UnnamedEntry(int column, int srcFile, int srcLine, int srcColumn) {
            super(column);
            this.srcFile = srcFile;
            this.srcLine = srcLine;
            this.srcColumn = srcColumn;
        }

        @Override
        public int getSourceFileId() {
            return srcFile;
        }

        @Override
        public int getSourceLine() {
            return srcLine;
        }

        @Override
        public int getSourceColumn() {
            return srcColumn;
        }

        @Override
        public int getNameId() {
            return UNMAPPED;
        }
    }

    /**
     * This class represents a portion of the generated file, that is mapped to a
     * section in the original source, and is associated with a name.
     */
    private static class NamedEntry extends UnnamedEntry {
        private final int name;

        NamedEntry(int column, int srcFile, int srcLine, int srcColumn, int name) {
            super(column, srcFile, srcLine, srcColumn);
            this.name = name;
        }

        @Override
        public int getNameId() {
            return name;
        }
    }

    public static interface EntryVisitor {
        void visit(String sourceName, String sourceContent, String symbolName, FilePosition sourceStartPosition,
                FilePosition startPosition, FilePosition endPosition);
    }

    public void visitMappings(EntryVisitor visitor) {
        boolean pending = false;
        String sourceName = null;
        String sourceContent = null;
        String symbolName = null;
        FilePosition sourceStartPosition = null;
        FilePosition startPosition = null;

        final int lineCount = lines.size();
        for (int i = 0; i < lineCount; i++) {
            ArrayList<Entry> line = lines.get(i);
            if (line != null) {
                final int entryCount = line.size();
                for (int j = 0; j < entryCount; j++) {
                    Entry entry = line.get(j);
                    if (pending) {
                        FilePosition endPosition = new FilePosition(i, entry.getGeneratedColumn());
                        visitor.visit(sourceName, sourceContent, symbolName, sourceStartPosition, startPosition,
                                endPosition);
                        pending = false;
                    }

                    if (entry.getSourceFileId() != UNMAPPED) {
                        pending = true;
                        sourceName = sources[entry.getSourceFileId()];
                        sourceContent = sourcesContent[entry.getSourceFileId()];
                        symbolName = (entry.getNameId() != UNMAPPED) ? names[entry.getNameId()] : null;
                        sourceStartPosition = new FilePosition(entry.getSourceLine(), entry.getSourceColumn());
                        startPosition = new FilePosition(i, entry.getGeneratedColumn());
                    }
                }
            }
        }
        //TODO: (closure report) (source map separation) I added this to because last mapping was never visited 
        if (pending) {
            FilePosition endPosition = new FilePosition(startPosition.getLine(), startPosition.getColumn());
            visitor.visit(sourceName, sourceContent, symbolName, sourceStartPosition, startPosition, endPosition);
        }

        //TODO: source map (closure report) - investigate and maybe fill bug to closure - they generate additional mappings to mark ends which is weird.

    }

}