com.github.sdbg.debug.core.internal.sourcemaps.SourceMap.java Source code

Java tutorial

Introduction

Here is the source code for com.github.sdbg.debug.core.internal.sourcemaps.SourceMap.java

Source

/*
 * Copyright (c) 2013, the Dart project authors.
 * 
 * Licensed under the Eclipse Public License v1.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.eclipse.org/legal/epl-v10.html
 * 
 * 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.sdbg.debug.core.internal.sourcemaps;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.text.NumberFormat;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IStorage;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.Path;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import com.github.sdbg.utilities.Streams;

// //@ sourceMappingURL=/path/to/file.js.map

/**
 * This maps from a generated file back to the original source files. It also supports the reverse
 * mapping; from locations in the source files to locations in the generated file.
 * 
 * @see http://www.html5rocks.com/en/tutorials/developertools/sourcemaps/
 */
public class SourceMap {

    public static final String SOURCE_MAP_EXT = ".map";

    public static SourceMap createFrom(File file) throws IOException {
        String contents = Streams.loadAndClose(new InputStreamReader(new FileInputStream(file), "UTF-8"));

        try {
            return createFrom(Path.fromOSString(file.getAbsolutePath()), contents);
        } catch (JSONException e) {
            throw new IOException(e);
        }
    }

    public static SourceMap createFrom(IStorage storage) throws IOException, CoreException {
        Reader reader;
        if (storage instanceof IFile) {
            reader = new InputStreamReader(storage.getContents(), ((IFile) storage).getCharset());
        } else {
            reader = new InputStreamReader(storage.getContents());
        }

        try {
            String contents = Streams.loadAndClose(reader);

            return createFrom(storage.getFullPath(), contents);
        } catch (JSONException e) {
            throw new IOException(e);
        } finally {
            reader.close();
        }
    }

    private static SourceMap createFrom(IPath path, JSONObject jsonObject) throws JSONException {
        return new SourceMap(path, jsonObject);
    }

    private static SourceMap createFrom(IPath path, String contents) throws JSONException {
        if (contents.startsWith(")]}")) {
            contents = contents.substring(3);
        }

        return createFrom(path, new JSONObject(contents));
    }

    private IPath path;

    /**
     * The format version; must be a positive integer. The current version of the spec is 3.
     */
    private int version;

    /**
     * The name of the generated code that this source map is associated with.
     */
    private String file;

    /**
     * An optional source root, useful for relocating source files on a server or removing repeated
     * values in the "sources" entry. This value is prepended to the individual entries in the
     * "source" field.
     */
    private String sourceRoot;

    /**
     * A list of original sources used by the "mappings" entry.
     */
    private String[] sources;

    /**
     * A list of symbol names used by the "mappings" entry.
     */
    private String[] names;

    /**
     * An optional list of source content, useful when the "source" cant be hosted.
     */
    @SuppressWarnings("unused")
    private String sourcesContent[];

    /**
     * The full list of source map entries.
     */
    private SourceMapInfoEntry[] entries;

    public SourceMap() {

    }

    public SourceMap(IPath path, JSONObject obj) throws JSONException {
        // {
        //     version : 3,
        //     file: "out.js",
        //     sourceRoot : "",
        //     sources: ["foo.js", "bar.js"],
        //     names: ["src", "maps", "are", "fun"],
        //     mappings: "AAgBC,SAAQ,CAAEA"
        // }

        this.path = path;

        version = obj.optInt("version");
        file = obj.optString("file");
        sourceRoot = obj.optString("sourceRoot");

        sources = parseStringArray(obj.getJSONArray("sources"));
        sourcesContent = parseStringArray(obj.optJSONArray("sourcesContent"));
        names = parseStringArray(obj.getJSONArray("names"));

        // Prepend sourceRoot to the sources entries.
        if (sourceRoot != null && sourceRoot.length() > 0) {
            for (int i = 0; i < sources.length; i++) {
                sources[i] = sourceRoot + sources[i];
            }
        }

        String mapStr = obj.getString("mappings");

        List<SourceMapInfoEntry> result = SourceMapDecoder.decode(sources, names, mapStr);
        entries = result.toArray(new SourceMapInfoEntry[result.size()]);
    }

    public String getFile() {
        return file;
    }

    /**
     * Map from a location in the generated file back to the original source.
     * 
     * @param line the line in the generated source
     * @param column the column in the generated source; -1 means the column is not interesting
     * @return the corresponding location in the original source
     */
    public SourceMapInfo getMappingFor(int line, int column) {
        int index = findIndexForLine(line);

        if (index == -1) {
            return null;
        }

        SourceMapInfoEntry entry = entries[index];

        // If column == -1, return the first mapping for that line.
        if (column == -1) {
            return entry.getInfo();
        }

        // Search for a matching mapping.
        while (index < entries.length) {
            entry = entries[index];

            if (entry.column <= column) {
                if (entry.endColumn == -1) {
                    return entry.getInfo();
                }

                if (column < entry.endColumn) {
                    return entry.getInfo();
                }
            }

            index++;
        }

        // no mapping found
        return null;
    }

    public IFile getMapSource() {
        String name = path.lastSegment();

        if (name.endsWith(SOURCE_MAP_EXT)) {
            name = name.substring(0, name.length() - SOURCE_MAP_EXT.length());

            IPath newPath = path.removeLastSegments(1).append(name);

            IResource resource = ResourcesPlugin.getWorkspace().getRoot().findMember(newPath);

            if (resource instanceof IFile) {
                return (IFile) resource;
            }
        }

        return null;
    }

    public IPath getPath() {
        return path;
    }

    /**
     * Map from a location in a source file to a location in the generated source file.
     * 
     * @param file
     * @param line
     * @param column
     * @return
     */
    public List<SourceMapInfo> getReverseMappingsFor(String file, int line) {
        // TODO(devoncarew): calculate this information once for O(1) lookup
        for (SourceMapInfoEntry entry : entries) {
            SourceMapInfo info = entry.getInfo();

            if (info == null) {
                continue;
            }

            if (line == info.getLine()) {
                if (file.equals(info.getFile())) {
                    // TODO(devoncarew): there will be several entries on this line
                    // We need to choose one that has a non-zero range, or is a catch-all entry

                    return Collections.singletonList(new SourceMapInfo(path.toString(), entry.line, entry.column));
                }
            }
        }

        return Collections.emptyList();
    }

    public String[] getSourceNames() {
        return sources;
    }

    /**
     * The format version; must be a positive integer. The current version of the specification is 3.
     */
    public int getVersion() {
        return version;
    }

    @Override
    public String toString() {
        return "[" + getPath().lastSegment() + ", " + NumberFormat.getNumberInstance().format(entries.length)
                + " lines]";
    }

    private int findIndexForLine(int line) {
        // TODO(devoncarew): test this binary search

        int location = Arrays.binarySearch(entries, SourceMapInfoEntry.forLine(line),
                SourceMapInfoEntry.lineComparator());

        if (location < 0) {
            return -1;
        }

        while (location > 0 && entries[location - 1].line == line) {
            location--;
        }

        return location;

        //    for (int i = 0; i < entries.length; i++) {
        //      SourceMapInfoEntry entry = entries[i];
        //
        //      if (entry.line == line) {
        //        return i;
        //      } else if (entry.line > line) {
        //        return -1;
        //      }
        //    }
        //
        //    return -1;
    }

    private String[] parseStringArray(JSONArray arr) throws JSONException {
        if (arr == null) {
            return null;
        } else {
            String[] strs = new String[arr.length()];

            for (int i = 0; i < arr.length(); i++) {
                strs[i] = arr.getString(i);
            }

            return strs;
        }
    }

}