JSConfiguration: add new --source-map-source-root compiler option

In source maps, instead of using the relative path from the original AS/MXML source files to the generated JS files, allows an optional custom path where all of the original source files may be found. In other words, allows the original source files to be moved from their original location at compile-time to a new location during debugging.
diff --git a/compiler-jx/src/main/java/org/apache/royale/compiler/clients/JSConfiguration.java b/compiler-jx/src/main/java/org/apache/royale/compiler/clients/JSConfiguration.java
index 938a9f3..874fdea 100644
--- a/compiler-jx/src/main/java/org/apache/royale/compiler/clients/JSConfiguration.java
+++ b/compiler-jx/src/main/java/org/apache/royale/compiler/clients/JSConfiguration.java
@@ -133,6 +133,25 @@
     }
 
     //
+    // 'source-map-source-root'
+    //
+
+    private String sourceMapSourceRoot = null;
+
+    public String getSourceMapSourceRoot()
+    {
+        return sourceMapSourceRoot;
+    }
+
+    @Config
+    @Mapping("source-map-source-root")
+    public void setSourceMapSourceRoot(ConfigurationValue cv, String value)
+            throws ConfigurationException
+    {
+        sourceMapSourceRoot = value;
+    }
+
+    //
     // 'js-default-initializers'
     //
 
diff --git a/compiler-jx/src/main/java/org/apache/royale/compiler/internal/graph/GoogDepsWriter.java b/compiler-jx/src/main/java/org/apache/royale/compiler/internal/graph/GoogDepsWriter.java
index da29ceb..5f2f828 100644
--- a/compiler-jx/src/main/java/org/apache/royale/compiler/internal/graph/GoogDepsWriter.java
+++ b/compiler-jx/src/main/java/org/apache/royale/compiler/internal/graph/GoogDepsWriter.java
@@ -50,6 +50,7 @@
 import org.apache.royale.compiler.problems.MainDefinitionQNameProblem;
 import org.apache.royale.compiler.problems.UnexpectedExceptionProblem;
 import org.apache.royale.compiler.units.ICompilationUnit;
+import org.apache.royale.compiler.utils.SourceMapUtils;
 import org.apache.royale.swc.ISWC;
 import org.apache.royale.swc.ISWCFileEntry;
 
@@ -68,6 +69,7 @@
 		this.mainName = mainClassName;
 		removeCirculars = config.getRemoveCirculars();
 		sourceMaps = config.getSourceMap();
+		sourceMapsSourceRoot = config.getSourceMapSourceRoot();
 		otherPaths = config.getSDKJSLib();
 		verbose = config.isVerbose();
 		otherPaths.add(new File(outputFolder.getParent(), "royale/Royale/src").getPath());
@@ -89,6 +91,7 @@
 	private List<ISWC> swcs;
 	private boolean removeCirculars = false;
 	private boolean sourceMaps = false;
+	private String sourceMapsSourceRoot = null;
 	private boolean verbose = false;
 	private ArrayList<GoogDep> dps;
 	private DependencyGraph graph;
@@ -122,7 +125,12 @@
 			else
 				files.add(gd.filePath);
 			visited.put(gd.className, gd);
+			if(sourceMaps && sourceMapsSourceRoot != null)
+			{
+				rewriteSourceMapSourceRoot(gd);
+			}
 		}
+		rewriteSourceMapSourceRoot(depMap.get(mainName));
 		if (removeCirculars)
 		{
 			GoogDep mainDep = depMap.get(mainName);
@@ -141,6 +149,55 @@
 		}
 		return files;
 	}
+
+	private void rewriteSourceMapSourceRoot(GoogDep gd)
+	{
+		if (!sourceMaps || sourceMapsSourceRoot == null || sourceMapsSourceRoot.length() == 0)
+		{
+			return;
+		}
+		File sourceMapFile = new File(gd.filePath + ".map");
+		if (!sourceMapFile.exists())
+		{
+			return;
+		}
+		String sourceMapContents = null;
+		try
+		{
+			sourceMapContents = FileUtils.readFileToString(sourceMapFile, Charset.forName("utf8"));
+		}
+		catch(IOException e)
+		{
+			return;
+		}
+		SourceMapConsumerV3 sourceMapConsumer = new SourceMapConsumerV3();
+		try
+		{
+			sourceMapConsumer.parse(sourceMapContents);
+		}
+		catch(SourceMapParseException e)
+		{
+			sourceMapConsumer = null;
+		}
+		if (sourceMapConsumer != null)
+		{
+			if (sourceMapsSourceRoot.equals(sourceMapConsumer.getSourceRoot()))
+			{
+				//no need to rewrite
+				return;
+			}
+			SourceMapGeneratorV3 sourceMapGenerator = SourceMapUtils.sourceMapConsumerToGeneratorWithRemappedSourceRoot(sourceMapConsumer, sourceMapsSourceRoot, gd.className);
+			String newSourceMapContents = SourceMapUtils.sourceMapGeneratorToString(sourceMapGenerator, new File(gd.filePath).getName());
+			try
+			{
+				FileUtils.write(sourceMapFile, newSourceMapContents, "utf8");
+			}
+			catch(IOException e)
+			{
+				return;
+			}
+		}
+	}
 	
 	public String generateDeps(CompilerProject project, ProblemQuery problems) throws FileNotFoundException
 	{
@@ -588,7 +645,7 @@
                 if (!isGoogProvided(s))
                 {
                 	fileLines.remove(j);
-					sourceMapConsumer = removeLineFromSourceMap(sourceMapConsumer, mainFile.getName(), j);
+					sourceMapConsumer = SourceMapUtils.removeLineFromSourceMap(sourceMapConsumer, mainFile.getName(), j);
                 }
 				else
 				{
@@ -606,15 +663,15 @@
 					.append(dep)
 					.append("');");
 				fileLines.add(main.fileInfo.googProvideLine + 1, lineBuilder.toString());
-				sourceMapConsumer = addLineToSourceMap(sourceMapConsumer, mainFile.getName(), main.fileInfo.googProvideLine + 1);
+				sourceMapConsumer = SourceMapUtils.addLineToSourceMap(sourceMapConsumer, mainFile.getName(), main.fileInfo.googProvideLine + 1);
 			}
 
 			FileUtils.writeLines(mainFile, "utf8", fileLines);
 
 			if (sourceMapConsumer != null)
 			{
-				String newSourceMap = sourceMapConsumerToString(sourceMapConsumer, mainFile.getName());
-				FileUtils.write(sourceMapFile, newSourceMap, "utf8");
+				String newSourceMapContents = SourceMapUtils.sourceMapConsumerToString(sourceMapConsumer, mainFile.getName());
+				FileUtils.write(sourceMapFile, newSourceMapContents, "utf8");
 			}
 		} catch (IOException e) {
 			// TODO Auto-generated catch block
@@ -759,7 +816,7 @@
                     			sb.append(",");
                     		sb.append(s);
 							firstDependency = false;
-							sourceMapConsumer = removeLineFromSourceMap(sourceMapConsumer, depFile.getName(), finalLines.size());
+							sourceMapConsumer = SourceMapUtils.removeLineFromSourceMap(sourceMapConsumer, depFile.getName(), finalLines.size());
                         	continue;
 	                    }
                         else
@@ -788,7 +845,7 @@
 							.append(dep)
 							.append("');");
             			finalLines.add(lastRequireLine++, lineBuilder.toString());
-						sourceMapConsumer = addLineToSourceMap(sourceMapConsumer, new File(gd.filePath).getName(), lastRequireLine);
+						sourceMapConsumer = SourceMapUtils.addLineToSourceMap(sourceMapConsumer, new File(gd.filePath).getName(), lastRequireLine);
             			if (verbose)
 						{
 							System.out.println("adding require for static dependency " + dep + " to " + className);
@@ -825,7 +882,7 @@
                 		{
                 			// there is already a fileOverview but no @suppress
                 			finalLines.add(fi.fileoverviewLine + 1, " *  @suppress {missingRequire}");
-							sourceMapConsumer = addLineToSourceMap(sourceMapConsumer, depFile.getName(), fi.fileoverviewLine + 1);
+							sourceMapConsumer = SourceMapUtils.addLineToSourceMap(sourceMapConsumer, depFile.getName(), fi.fileoverviewLine + 1);
                 		}
                 		else if (fi.googProvideLine > -1)
                 		{
@@ -833,10 +890,10 @@
                 			finalLines.add(fi.googProvideLine, " *  @suppress {missingRequire}");
                 			finalLines.add(fi.googProvideLine, " *  @fileoverview");
                 			finalLines.add(fi.googProvideLine, "/**");
-							sourceMapConsumer = addLineToSourceMap(sourceMapConsumer, depFile.getName(), fi.googProvideLine);
-							sourceMapConsumer = addLineToSourceMap(sourceMapConsumer, depFile.getName(), fi.googProvideLine);
-							sourceMapConsumer = addLineToSourceMap(sourceMapConsumer, depFile.getName(), fi.googProvideLine);
-							sourceMapConsumer = addLineToSourceMap(sourceMapConsumer, depFile.getName(), fi.googProvideLine);
+							sourceMapConsumer = SourceMapUtils.addLineToSourceMap(sourceMapConsumer, depFile.getName(), fi.googProvideLine);
+							sourceMapConsumer = SourceMapUtils.addLineToSourceMap(sourceMapConsumer, depFile.getName(), fi.googProvideLine);
+							sourceMapConsumer = SourceMapUtils.addLineToSourceMap(sourceMapConsumer, depFile.getName(), fi.googProvideLine);
+							sourceMapConsumer = SourceMapUtils.addLineToSourceMap(sourceMapConsumer, depFile.getName(), fi.googProvideLine);
                 		}
                 		else
                 		{
@@ -850,7 +907,7 @@
             		{
             			// there is already a fileoverview but no @suppress
             			finalLines.add(fi.fileoverviewLine + 1, " *  @suppress {missingRequire}");
-						sourceMapConsumer = addLineToSourceMap(sourceMapConsumer, depFile.getName(), fi.fileoverviewLine + 1);
+						sourceMapConsumer = SourceMapUtils.addLineToSourceMap(sourceMapConsumer, depFile.getName(), fi.fileoverviewLine + 1);
             		}
             		else if (fi.googProvideLine > -1)
             		{
@@ -858,10 +915,10 @@
             			finalLines.add(fi.googProvideLine, " *  @suppress {missingRequire}");
             			finalLines.add(fi.googProvideLine, " *  @fileoverview");
             			finalLines.add(fi.googProvideLine, "/**");
-						sourceMapConsumer = addLineToSourceMap(sourceMapConsumer, depFile.getName(), fi.googProvideLine);
-						sourceMapConsumer = addLineToSourceMap(sourceMapConsumer, depFile.getName(), fi.googProvideLine);
-						sourceMapConsumer = addLineToSourceMap(sourceMapConsumer, depFile.getName(), fi.googProvideLine);
-						sourceMapConsumer = addLineToSourceMap(sourceMapConsumer, depFile.getName(), fi.googProvideLine);
+						sourceMapConsumer = SourceMapUtils.addLineToSourceMap(sourceMapConsumer, depFile.getName(), fi.googProvideLine);
+						sourceMapConsumer = SourceMapUtils.addLineToSourceMap(sourceMapConsumer, depFile.getName(), fi.googProvideLine);
+						sourceMapConsumer = SourceMapUtils.addLineToSourceMap(sourceMapConsumer, depFile.getName(), fi.googProvideLine);
+						sourceMapConsumer = SourceMapUtils.addLineToSourceMap(sourceMapConsumer, depFile.getName(), fi.googProvideLine);
             		}
             		else
             		{
@@ -872,14 +929,14 @@
 
             sb.append("*/");
             finalLines.add(gd.fileInfo.googProvideLine + 1, sb.toString());
-			sourceMapConsumer = addLineToSourceMap(sourceMapConsumer, depFile.getName(), gd.fileInfo.googProvideLine + 1);
+			sourceMapConsumer = SourceMapUtils.addLineToSourceMap(sourceMapConsumer, depFile.getName(), gd.fileInfo.googProvideLine + 1);
 
 			FileUtils.writeLines(depFile, "utf8", finalLines);
 
 			if (sourceMapConsumer != null)
 			{
-				String newSourceMap = sourceMapConsumerToString(sourceMapConsumer, depFile.getName());
-				FileUtils.write(sourceMapFile, newSourceMap, "utf8");
+				String newSourceMapContents = SourceMapUtils.sourceMapConsumerToString(sourceMapConsumer, depFile.getName());
+				FileUtils.write(sourceMapFile, newSourceMapContents, "utf8");
 			}
         }
         catch (IOException e)
@@ -887,187 +944,6 @@
             e.printStackTrace();
         }		
 	}
-
-	String sourceMapConsumerToString(SourceMapConsumerV3 consumer, String file)
-	{
-		SourceMapGeneratorV3 generator = sourceMapConsumerToGenerator(consumer);
-		StringBuilder builder = new StringBuilder();
-		try
-		{
-			generator.appendTo(builder, file);
-		}
-		catch(IOException e)
-		{
-			return "";
-		}
-		return builder.toString();
-	}
-
-	private void appendExtraMappingToGenerator(SourceMapGeneratorV3 generator,
-		String sourceName,
-		String symbolName,
-		FilePosition sourceStartPosition,
-		FilePosition startPosition,
-		FilePosition endPosition)
-	{
-		//add an extra mapping because there seems to be a bug in
-		//SourceMapGeneratorV3's appendTo() that omits the last
-		//entry, for some reason
-		FilePosition newEndPosition = new FilePosition(endPosition.getLine(), endPosition.getColumn() + 1);
-		generator.addMapping(sourceName, null, sourceStartPosition, endPosition, newEndPosition);
-	}
-
-	private SourceMapGeneratorV3 sourceMapConsumerToGenerator(SourceMapConsumerV3 consumer)
-	{
-		final SourceMapGeneratorV3 generator = new SourceMapGeneratorV3();
-		final SourceMapEntryCounter counter = new SourceMapEntryCounter();
-		generator.setSourceRoot(consumer.getSourceRoot());
-		consumer.visitMappings(counter);
-		consumer.visitMappings(new SourceMapConsumerV3.EntryVisitor()
-		{
-			private int index = 0;
-
-			@Override
-			public void visit(String sourceName,
-				String symbolName,
-				FilePosition sourceStartPosition,
-				FilePosition startPosition,
-				FilePosition endPosition) {
-				generator.addMapping(sourceName, symbolName, sourceStartPosition, startPosition, endPosition);
-				index++;
-				if(index == counter.count)
-				{
-					//add an extra mapping because there seems to be a bug in
-					//SourceMapGeneratorV3's appendTo() that omits the last
-					//entry, for some reason
-					appendExtraMappingToGenerator(generator, sourceName, symbolName, sourceStartPosition, startPosition, endPosition);
-				}
-			}
-		});
-		return generator;
-	}
-
-	class SourceMapEntryCounter implements SourceMapConsumerV3.EntryVisitor
-	{
-		private int count = 0;
-
-		@Override
-		public void visit(String sourceName,
-			String symbolName,
-			FilePosition sourceStartPosition,
-			FilePosition startPosition,
-			FilePosition endPosition) {
-			count++;
-		}
-	}
-
-	SourceMapConsumerV3 sourceMapGeneratorToConsumer(SourceMapGeneratorV3 generator, String fileName)
-	{
-		StringBuilder builder = new StringBuilder();
-		try
-		{
-			generator.appendTo(builder, fileName);
-		}
-		catch(IOException e)
-		{
-			return null;
-		}
-		SourceMapConsumerV3 consumer = new SourceMapConsumerV3();
-		try
-		{
-			consumer.parse(builder.toString());
-		}
-		catch(SourceMapParseException e)
-		{
-			return null;
-		}
-		return consumer;
-	}
-
-	SourceMapConsumerV3 addLineToSourceMap(SourceMapConsumerV3 consumer, String sourceFileName, final int lineToAdd)
-	{
-		if (consumer == null)
-		{
-			return null;
-		}
-		final SourceMapGeneratorV3 generator = new SourceMapGeneratorV3();
-		final SourceMapEntryCounter counter = new SourceMapEntryCounter();
-		generator.setSourceRoot(consumer.getSourceRoot());
-		consumer.visitMappings(counter);
-		consumer.visitMappings(new SourceMapConsumerV3.EntryVisitor()
-		{
-			private int index = 0;
-
-			@Override
-			public void visit(String sourceName,
-				String symbolName,
-				FilePosition sourceStartPosition,
-				FilePosition startPosition,
-				FilePosition endPosition) {
-				if(startPosition.getLine() >= lineToAdd)
-				{
-					startPosition = new FilePosition(startPosition.getLine() + 1, startPosition.getColumn());
-					endPosition = new FilePosition(endPosition.getLine() + 1, endPosition.getColumn());
-				}
-				generator.addMapping(sourceName, symbolName, sourceStartPosition, startPosition, endPosition);
-				index++;
-				if(index == counter.count)
-				{
-					//add an extra mapping because there seems to be a bug in
-					//SourceMapGeneratorV3's appendTo() that omits the last
-					//entry, for some reason
-					appendExtraMappingToGenerator(generator, sourceName, symbolName, sourceStartPosition, startPosition, endPosition);
-				}
-			}
-		});
-		return sourceMapGeneratorToConsumer(generator, sourceFileName);
-	}
-
-	SourceMapConsumerV3 removeLineFromSourceMap(SourceMapConsumerV3 consumer, String sourceFileName, final int lineToRemove)
-	{
-		if (consumer == null)
-		{
-			return null;
-		}
-		final SourceMapGeneratorV3 generator = new SourceMapGeneratorV3();
-		final SourceMapEntryCounter counter = new SourceMapEntryCounter();
-		generator.setSourceRoot(consumer.getSourceRoot());
-		consumer.visitMappings(counter);
-		consumer.visitMappings(new SourceMapConsumerV3.EntryVisitor()
-		{
-			private int index = 0;
-
-			@Override
-			public void visit(String sourceName,
-				String symbolName,
-				FilePosition sourceStartPosition,
-				FilePosition startPosition,
-				FilePosition endPosition) {
-				if(startPosition.getLine() == lineToRemove)
-				{
-					return;
-				}
-				if(startPosition.getLine() > lineToRemove)
-				{
-					startPosition = new FilePosition(startPosition.getLine() - 1, startPosition.getColumn());
-				}
-				if(endPosition.getLine() > lineToRemove)
-				{
-					endPosition = new FilePosition(endPosition.getLine() - 1, endPosition.getColumn());
-				}
-				generator.addMapping(sourceName, symbolName, sourceStartPosition, startPosition, endPosition);
-				index++;
-				if(index == counter.count)
-				{
-					//add an extra mapping because there seems to be a bug in
-					//SourceMapGeneratorV3's appendTo() that omits the last
-					//entry, for some reason
-					appendExtraMappingToGenerator(generator, sourceName, symbolName, sourceStartPosition, startPosition, endPosition);
-				}
-			}
-		});
-		return sourceMapGeneratorToConsumer(generator, sourceFileName);
-	}
 		
 	FileInfo getFileInfo(List<String> lines, String className)
 	{
@@ -1353,42 +1229,33 @@
 							File sourceMapDestFile = new File(sourceMapFn);
 							inStream = sourceMapFileEntry.createInputStream();
 							String sourceMapContents = IOUtils.toString(inStream, Charset.forName("utf8"));
-							SourceMapConsumerV3 sourceMapConsumer = new SourceMapConsumerV3();
-							try
+							if (sourceMapsSourceRoot == null)
 							{
-								sourceMapConsumer.parse(sourceMapContents);
-							}
-							catch(SourceMapParseException e)
-							{
-								sourceMapConsumer = null;
-							}
-							if(sourceMapConsumer != null)
-							{
-								String sourceRoot = sourceMapConsumer.getSourceRoot();
-								int index = sourceRoot.indexOf("/frameworks/js/projects/");
-								if(index != -1)
+								SourceMapConsumerV3 sourceMapConsumer = new SourceMapConsumerV3();
+								try
 								{
-									File royalelib = new File(System.getProperty("royalelib"));
-									File newSourceRoot = new File(royalelib.getParent(), sourceRoot.substring(index + 1));
-									SourceMapGeneratorV3 sourceMapGenerator = sourceMapConsumerToGenerator(sourceMapConsumer);
-									String newSourceRootUri = convertSourcePathToURI(newSourceRoot.getAbsolutePath());
-									sourceMapGenerator.setSourceRoot(newSourceRootUri);
-									StringBuilder builder = new StringBuilder();
-									try
-									{
-										sourceMapGenerator.appendTo(builder, destFile.getName());
-									}
-									catch(IOException e)
-									{
-										return "";
-									}
-									FileUtils.writeStringToFile(sourceMapDestFile, builder.toString(), Charset.forName("utf8"));
+									sourceMapConsumer.parse(sourceMapContents);
 								}
-								else
+								catch(SourceMapParseException e)
 								{
-									FileUtils.writeStringToFile(sourceMapDestFile, sourceMapContents, Charset.forName("utf8"));
+									sourceMapConsumer = null;
+								}
+								if(sourceMapConsumer != null)
+								{
+									String sourceRoot = sourceMapConsumer.getSourceRoot();
+									int index = sourceRoot.indexOf("/frameworks/js/projects/");
+									if(index != -1)
+									{
+										File royalelib = new File(System.getProperty("royalelib"));
+										File newSourceRoot = new File(royalelib.getParent(), sourceRoot.substring(index + 1));
+										SourceMapGeneratorV3 sourceMapGenerator = SourceMapUtils.sourceMapConsumerToGenerator(sourceMapConsumer);
+										String newSourceRootUri = convertSourcePathToURI(newSourceRoot.getAbsolutePath());
+										sourceMapGenerator.setSourceRoot(newSourceRootUri);
+										sourceMapContents = SourceMapUtils.sourceMapGeneratorToString(sourceMapGenerator, destFile.getName());
+									}
 								}
 							}
+							FileUtils.writeStringToFile(sourceMapDestFile, sourceMapContents, Charset.forName("utf8"));
 						}
 					}
 
diff --git a/compiler-jx/src/main/java/org/apache/royale/compiler/utils/SourceMapUtils.java b/compiler-jx/src/main/java/org/apache/royale/compiler/utils/SourceMapUtils.java
new file mode 100644
index 0000000..ba2d9bb
--- /dev/null
+++ b/compiler-jx/src/main/java/org/apache/royale/compiler/utils/SourceMapUtils.java
@@ -0,0 +1,257 @@
+/*
+ *
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You 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 org.apache.royale.compiler.utils;
+
+import java.io.IOException;
+
+import com.google.debugging.sourcemap.FilePosition;
+import com.google.debugging.sourcemap.SourceMapConsumerV3;
+import com.google.debugging.sourcemap.SourceMapGeneratorV3;
+import com.google.debugging.sourcemap.SourceMapParseException;
+
+public class SourceMapUtils
+{
+	public static String sourceMapConsumerToString(SourceMapConsumerV3 consumer, String file)
+	{
+		SourceMapGeneratorV3 generator = sourceMapConsumerToGenerator(consumer);
+		StringBuilder builder = new StringBuilder();
+		try
+		{
+			generator.appendTo(builder, file);
+		}
+		catch(IOException e)
+		{
+			return "";
+		}
+		return builder.toString();
+	}
+
+	public static SourceMapGeneratorV3 sourceMapConsumerToGenerator(SourceMapConsumerV3 consumer)
+	{
+		final SourceMapGeneratorV3 generator = new SourceMapGeneratorV3();
+		final SourceMapEntryCounter counter = new SourceMapEntryCounter();
+		generator.setSourceRoot(consumer.getSourceRoot());
+		consumer.visitMappings(counter);
+		consumer.visitMappings(new SourceMapConsumerV3.EntryVisitor()
+		{
+			private int index = 0;
+
+			@Override
+			public void visit(String sourceName,
+				String symbolName,
+				FilePosition sourceStartPosition,
+				FilePosition startPosition,
+				FilePosition endPosition) {
+				generator.addMapping(sourceName, symbolName, sourceStartPosition, startPosition, endPosition);
+				index++;
+				if(index == counter.count)
+				{
+					//add an extra mapping because there seems to be a bug in
+					//SourceMapGeneratorV3's appendTo() that omits the last
+					//entry, for some reason
+					appendExtraMappingToGenerator(generator, sourceName, symbolName, sourceStartPosition, startPosition, endPosition);
+				}
+			}
+		});
+		return generator;
+	}
+
+	public static SourceMapGeneratorV3 sourceMapConsumerToGeneratorWithRemappedSourceRoot(SourceMapConsumerV3 consumer, String sourceRoot, String className)
+	{
+		final SourceMapGeneratorV3 generator = new SourceMapGeneratorV3();
+		final SourceMapEntryCounter counter = new SourceMapEntryCounter();
+		final String startPath = "/" + className.replace(".", "/") + ".";
+		generator.setSourceRoot(sourceRoot);
+		consumer.visitMappings(counter);
+		consumer.visitMappings(new SourceMapConsumerV3.EntryVisitor()
+		{
+			private int index = 0;
+
+			@Override
+			public void visit(String sourceName,
+					String symbolName,
+					FilePosition sourceStartPosition,
+					FilePosition startPosition,
+					FilePosition endPosition) {
+				String newSourceName = sourceName;
+				int startPathIndex = newSourceName.indexOf(startPath);
+				if(startPathIndex != -1)
+				{
+					newSourceName = newSourceName.substring(startPathIndex + 1);
+				}
+				generator.addMapping(newSourceName, symbolName, sourceStartPosition, startPosition, endPosition);
+				index++;
+				if(index == counter.count)
+				{
+					//add an extra mapping because there seems to be a bug in
+					//SourceMapGeneratorV3's appendTo() that omits the last
+					//entry, for some reason
+					appendExtraMappingToGenerator(generator, newSourceName, symbolName, sourceStartPosition, startPosition, endPosition);
+				}
+			}
+		});
+		return generator;
+	}
+
+	public static SourceMapConsumerV3 addLineToSourceMap(SourceMapConsumerV3 consumer, String sourceFileName, final int lineToAdd)
+	{
+		if (consumer == null)
+		{
+			return null;
+		}
+		final SourceMapGeneratorV3 generator = new SourceMapGeneratorV3();
+		final SourceMapEntryCounter counter = new SourceMapEntryCounter();
+		generator.setSourceRoot(consumer.getSourceRoot());
+		consumer.visitMappings(counter);
+		consumer.visitMappings(new SourceMapConsumerV3.EntryVisitor()
+		{
+			private int index = 0;
+
+			@Override
+			public void visit(String sourceName,
+				String symbolName,
+				FilePosition sourceStartPosition,
+				FilePosition startPosition,
+				FilePosition endPosition) {
+				if(startPosition.getLine() >= lineToAdd)
+				{
+					startPosition = new FilePosition(startPosition.getLine() + 1, startPosition.getColumn());
+					endPosition = new FilePosition(endPosition.getLine() + 1, endPosition.getColumn());
+				}
+				generator.addMapping(sourceName, symbolName, sourceStartPosition, startPosition, endPosition);
+				index++;
+				if(index == counter.count)
+				{
+					//add an extra mapping because there seems to be a bug in
+					//SourceMapGeneratorV3's appendTo() that omits the last
+					//entry, for some reason
+					appendExtraMappingToGenerator(generator, sourceName, symbolName, sourceStartPosition, startPosition, endPosition);
+				}
+			}
+		});
+		return sourceMapGeneratorToConsumer(generator, sourceFileName);
+	}
+
+	public static SourceMapConsumerV3 removeLineFromSourceMap(SourceMapConsumerV3 consumer, String sourceFileName, final int lineToRemove)
+	{
+		if (consumer == null)
+		{
+			return null;
+		}
+		final SourceMapGeneratorV3 generator = new SourceMapGeneratorV3();
+		final SourceMapEntryCounter counter = new SourceMapEntryCounter();
+		generator.setSourceRoot(consumer.getSourceRoot());
+		consumer.visitMappings(counter);
+		consumer.visitMappings(new SourceMapConsumerV3.EntryVisitor()
+		{
+			private int index = 0;
+
+			@Override
+			public void visit(String sourceName,
+				String symbolName,
+				FilePosition sourceStartPosition,
+				FilePosition startPosition,
+				FilePosition endPosition) {
+				if(startPosition.getLine() == lineToRemove)
+				{
+					return;
+				}
+				if(startPosition.getLine() > lineToRemove)
+				{
+					startPosition = new FilePosition(startPosition.getLine() - 1, startPosition.getColumn());
+				}
+				if(endPosition.getLine() > lineToRemove)
+				{
+					endPosition = new FilePosition(endPosition.getLine() - 1, endPosition.getColumn());
+				}
+				generator.addMapping(sourceName, symbolName, sourceStartPosition, startPosition, endPosition);
+				index++;
+				if(index == counter.count)
+				{
+					//add an extra mapping because there seems to be a bug in
+					//SourceMapGeneratorV3's appendTo() that omits the last
+					//entry, for some reason
+					appendExtraMappingToGenerator(generator, sourceName, symbolName, sourceStartPosition, startPosition, endPosition);
+				}
+			}
+		});
+		return sourceMapGeneratorToConsumer(generator, sourceFileName);
+	}
+
+	private static void appendExtraMappingToGenerator(SourceMapGeneratorV3 generator,
+		String sourceName,
+		String symbolName,
+		FilePosition sourceStartPosition,
+		FilePosition startPosition,
+		FilePosition endPosition)
+	{
+		//add an extra mapping because there seems to be a bug in
+		//SourceMapGeneratorV3's appendTo() that omits the last
+		//entry, for some reason
+		FilePosition newEndPosition = new FilePosition(endPosition.getLine(), endPosition.getColumn() + 1);
+		generator.addMapping(sourceName, null, sourceStartPosition, endPosition, newEndPosition);
+	}
+
+	private static class SourceMapEntryCounter implements SourceMapConsumerV3.EntryVisitor
+	{
+		private int count = 0;
+
+		@Override
+		public void visit(String sourceName,
+			String symbolName,
+			FilePosition sourceStartPosition,
+			FilePosition startPosition,
+			FilePosition endPosition) {
+			count++;
+		}
+	}
+
+	public static String sourceMapGeneratorToString(SourceMapGeneratorV3 generator, String fileName)
+	{
+		StringBuilder builder = new StringBuilder();
+		try
+		{
+			generator.appendTo(builder, fileName);
+		}
+		catch(IOException e)
+		{
+			return null;
+		}
+		return builder.toString();
+	}
+
+	public static SourceMapConsumerV3 sourceMapGeneratorToConsumer(SourceMapGeneratorV3 generator, String fileName)
+	{
+		String generatorString = sourceMapGeneratorToString(generator, fileName);
+		if(generatorString == null)
+		{
+			return null;
+		}
+		SourceMapConsumerV3 consumer = new SourceMapConsumerV3();
+		try
+		{
+			consumer.parse(generatorString);
+		}
+		catch(SourceMapParseException e)
+		{
+			return null;
+		}
+		return consumer;
+	}
+}