blob: d2577cb6add833dd134029c8782ad6d4e6613dad [file] [log] [blame]
/*
* 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 flash.tools.debugger.concrete;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import flash.tools.debugger.NoResponseException;
import flash.tools.debugger.Session;
import flash.tools.debugger.SourceFile;
import flash.tools.debugger.SourceLocator;
import flash.tools.debugger.VersionException;
import flash.util.FileUtils;
/**
* A module which is uniquly identified by an id, contains
* a short and long name and also a script
*/
public class DModule implements SourceFile
{
private ScriptText m_script; // lazy-initialized by getScript()
private boolean m_gotRealScript;
private final String m_rawName;
private final String m_shortName;
private final String m_path;
private final String m_basePath;
private final int m_id;
private final int m_bitmap;
private final ArrayList<Integer> m_line2Offset;
private final ArrayList<Object> m_line2Func; // each array is either null, String, or String[]
private final HashMap<String, Integer> m_func2FirstLine; // maps function name (String) to first line of function (Integer)
private final HashMap<String, Integer> m_func2LastLine; // maps function name (String) to last line of function (Integer)
private String m_packageName;
private boolean m_gotAllFncNames;
private int m_anonymousFunctionCounter = 0;
private SourceLocator m_sourceLocator;
private int m_sourceLocatorChangeCount;
private int m_isolateId;
private final static String m_newline = System.getProperty("line.separator"); //$NON-NLS-1$
/**
* @param name filename in "basepath;package;filename" format
*/
public DModule(SourceLocator sourceLocator, int id, int bitmap, String name, String script, int isolateId)
{
// If the caller gave us the script text, then we will create m_script
// now. But if the caller gave us an empty string, then we won't bother
// looking for a disk file until someone actually asks for it.
if (script != null && script.length() > 0)
{
m_script = new ScriptText(script);
m_gotRealScript = true;
}
NameParser nameParser = new NameParser(name);
m_sourceLocator = sourceLocator;
m_rawName = name;
m_basePath = nameParser.getBasePath(); // may be null
m_bitmap = bitmap;
m_id = id;
m_shortName = generateShortName(nameParser);
m_path = generatePath(nameParser);
m_line2Offset = new ArrayList<Integer>();
m_line2Func = new ArrayList<Object>();
m_func2FirstLine = new HashMap<String, Integer>();
m_func2LastLine = new HashMap<String, Integer>();
m_packageName = nameParser.getPackage();
m_gotAllFncNames = false;
m_isolateId = isolateId;
}
public synchronized ScriptText getScript()
{
// If we have been using "dummy" source, and the user has changed the list of
// directories that are searched for source, then we want to search again
if (!m_gotRealScript &&
m_sourceLocator != null &&
m_sourceLocator.getChangeCount() != m_sourceLocatorChangeCount)
{
m_script = null;
}
// lazy-initialize m_script, so that we don't read a disk file until
// someone actually needs to look at the file
if (m_script == null)
{
String script = scriptFromDisk(getRawName());
if (script == null)
{
script = ""; // use dummy source for now //$NON-NLS-1$
}
else
{
m_gotRealScript = true; // we got the real source
}
m_script = new ScriptText(script);
}
return m_script;
}
/* getters */
public String getBasePath() { return m_basePath; }
public String getName() { return m_shortName; }
public String getFullPath() { return m_path; }
public String getPackageName() { return (m_packageName == null) ? "" : m_packageName; } //$NON-NLS-1$
public String getRawName() { return m_rawName; }
public int getId() { return m_id; }
public int getBitmap() { return m_bitmap; }
public int getLineCount() { return getScript().getLineCount(); }
public String getLine(int i) { return (i > getLineCount()) ? "// code goes here" : getScript().getLine(i); } //$NON-NLS-1$
void setPackageName(String name) { m_packageName = name; }
/**
* @return the offset within the swf for a given line
* of source. 0 if unknown.
*/
public int getOffsetForLine(int line)
{
int offset = 0;
if (line < m_line2Offset.size())
{
Integer i = m_line2Offset.get(line);
if (i != null)
offset = i.intValue();
}
return offset;
}
public int getLineForFunctionName(Session s, String name)
{
int value = -1;
primeAllFncNames(s);
Integer i = m_func2FirstLine.get(name);
if (i != null)
value = i.intValue();
return value;
}
/*
* @see flash.tools.debugger.SourceFile#getFunctionNameForLine(flash.tools.debugger.Session, int)
*/
public String getFunctionNameForLine(Session s, int line)
{
primeFncName(s, line);
String[] funcNames = getFunctionNamesForLine(s, line);
if (funcNames != null && funcNames.length == 1)
return funcNames[0];
else
return null;
}
/**
* Return the function names for a given line number, or an empty array
* if there are none; never returns <code>null</code>.
*/
private String[] getFunctionNamesForLine(Session s, int line)
{
primeFncName(s, line);
if (line < m_line2Func.size())
{
Object obj = m_line2Func.get(line);
if (obj instanceof String)
return new String[] { (String) obj };
else if (obj instanceof String[])
return (String[]) obj;
}
return new String[0];
}
public String[] getFunctionNames(Session s)
{
/* find out the size of the array */
primeAllFncNames(s);
int count = m_func2FirstLine.size();
return m_func2FirstLine.keySet().toArray(new String[count]);
}
private static String generateShortName(NameParser nameParser)
{
String name = nameParser.getOriginalName();
String s = name;
if (nameParser.isPathPackageAndFilename()) {
s = nameParser.getFilename();
} else {
/* do we have a file name? */
int dotAt = name.lastIndexOf('.');
if (dotAt > 1)
{
/* yes let's strip the directory off */
int lastSlashAt = name.lastIndexOf('\\', dotAt);
lastSlashAt = Math.max(lastSlashAt, name.lastIndexOf('/', dotAt));
s = name.substring(lastSlashAt+1);
}
else
{
/* not a file name ... */
s = name;
}
}
return s.trim();
}
/**
* Produce a name that contains a file specification including full path.
* File names may come in as 'mx.bla : file:/bla.foo.as' or as
* 'file://bla.foo.as' or as 'C:\'(?) or as 'basepath;package;filename'
*/
private static String generatePath(NameParser nameParser)
{
String name = nameParser.getOriginalName();
String s = name;
/* strip off first colon of stuff if package exists */
int colonAt = name.indexOf(':');
if (colonAt > 1 && !name.startsWith("Actions for")) //$NON-NLS-1$
{
if (name.charAt(colonAt+1) == ' ')
s = name.substring(colonAt+2);
}
else if (name.indexOf('.') > -1 && name.charAt(0) != '<' )
{
/* some other type of file name */
s = nameParser.recombine();
}
else
{
// no path
s = ""; //$NON-NLS-1$
}
return s.trim();
}
public void lineMapping(StringBuilder sb)
{
Map<String, String> args = new HashMap<String, String>();
args.put("fileName", getName() ); //$NON-NLS-1$
args.put("fileNumber", Integer.toString(getId()) ); //$NON-NLS-1$
sb.append(PlayerSessionManager.getLocalizationManager().getLocalizedTextString("functionsInFile", args)); //$NON-NLS-1$
sb.append(m_newline);
String[] funcNames = m_func2FirstLine.keySet().toArray(new String[m_func2FirstLine.size()]);
Arrays.sort(funcNames, new Comparator<String>() {
public int compare(String o1, String o2) {
int line1 = m_func2FirstLine.get(o1).intValue();
int line2 = m_func2FirstLine.get(o2).intValue();
return line1 - line2;
}
});
for (int i=0; i<funcNames.length; ++i)
{
String name = funcNames[i];
int firstLine = m_func2FirstLine.get(name).intValue();
int lastLine = m_func2LastLine.get(name).intValue();
sb.append(" "); //$NON-NLS-1$
sb.append(name);
sb.append(" "); //$NON-NLS-1$
sb.append(firstLine);
sb.append(" "); //$NON-NLS-1$
sb.append(lastLine);
sb.append(m_newline);
}
}
int compareTo(DModule other)
{
return getName().compareTo(other.getName());
}
/**
* Called in order to make sure that we have a function name available
* at the given location. For AVM+ swfs we don't need a swd and therefore
* don't have access to function names in the same fashion.
* We need to ask the player for a particular function name.
*/
void primeFncName(Session s, int line)
{
// for now we do all, optimize later
primeAllFncNames(s);
}
void primeAllFncNames(Session s)
{
// send out the request for all functions that the player knows
// about for this module
// we block on this call waiting for an answer and after we get it
// the DManager thread should have populated our mapping tables
// under the covers. If its fails then no biggie we just won't
// see anything in the tables.
PlayerSession ps = (PlayerSession)s;
if (!m_gotAllFncNames && ps.playerVersion() >= 9)
{
try
{
ps.requestFunctionNames(m_id, -1, m_isolateId);
}
catch (VersionException e)
{
;
}
catch (NoResponseException e)
{
;
}
}
m_gotAllFncNames = true;
}
void addLineFunctionInfo(int offset, int line, String funcName)
{
addLineFunctionInfo(offset, line, line, funcName);
}
/**
* Called by DSwfInfo in order to add function name / line / offset mapping
* information to the module.
*/
void addLineFunctionInfo(int offset, int firstLine, int lastLine, String funcName)
{
int line;
// strip down the function name
if (funcName == null || funcName.length() == 0)
{
funcName = "<anonymous$" + (++m_anonymousFunctionCounter) + ">"; //$NON-NLS-1$ //$NON-NLS-2$
}
else
{
// colons or slashes then this is an AS3 name, strip off the core::
int colon = funcName.lastIndexOf(':');
int slash = funcName.lastIndexOf('/');
if (colon > -1 || slash > -1)
{
int greater = Math.max(colon, slash);
funcName = funcName.substring(greater+1);
}
else
{
int dot = funcName.lastIndexOf('.');
if (dot > -1)
{
// extract function and package
String pkg = funcName.substring(0, dot);
funcName = funcName.substring(dot+1);
// attempt to set the package name while we're in here
setPackageName(pkg);
// System.out.println(m_id+"-func="+funcName+",pkg="+pkg);
}
}
}
// System.out.println(m_id+"@"+offset+"="+getPath()+".adding func="+funcName);
// make sure m_line2Offset is big enough for the lines we're about to set
m_line2Offset.ensureCapacity(firstLine+1);
while (firstLine >= m_line2Offset.size())
m_line2Offset.add(null);
// add the offset mapping
m_line2Offset.set(firstLine, new Integer(offset));
// make sure m_line2Func is big enough for the lines we're about to se
m_line2Func.ensureCapacity(lastLine+1);
while (lastLine >= m_line2Func.size())
m_line2Func.add(null);
// offset and byteCode ignored currently, only add the name for the first hit
for (line = firstLine; line <= lastLine; ++line)
{
Object funcs = m_line2Func.get(line);
// A line can correspond to more than one function. The most common case
// of that is an MXML tag with two event handlers on the same line, e.g.
// <mx:Button mouseOver="overHandler()" mouseOut="outHandler()" />;
// another case is the line that declares an inner anonymous function:
// var f:Function = function() { trace('hi') }
// In any such case, we store a list of function names separated by commas,
// e.g. "func1, func2"
if (funcs == null)
{
m_line2Func.set(line, funcName);
}
else if (funcs instanceof String)
{
String oldFunc = (String) funcs;
m_line2Func.set(line, new String[] { oldFunc, funcName });
}
else if (funcs instanceof String[])
{
String[] oldFuncs = (String[]) funcs;
String[] newFuncs = new String[oldFuncs.length + 1];
System.arraycopy(oldFuncs, 0, newFuncs, 0, oldFuncs.length);
newFuncs[newFuncs.length - 1] = funcName;
m_line2Func.set(line, newFuncs);
}
}
// add to our function name list
if (m_func2FirstLine.get(funcName) == null)
{
m_func2FirstLine.put(funcName, new Integer(firstLine));
m_func2LastLine.put(funcName, new Integer(lastLine));
}
}
/**
* Scan the disk looking for the location of where the source resides. May
* also peel open a swd file looking for the source file.
* @param name original full path name of the source file
* @return string containing the contents of the file, or null if not found
*/
private String scriptFromDisk(String name)
{
// we expect the form of the filename to be in the form
// "c:/src/project;debug;myFile.as"
// where the semicolons demark the include directory searched by the
// compiler followed by package directories then file name.
// any slashes are to be forward slash only!
// translate to neutral form
name = name.replace('\\','/'); //@todo remove this when compiler is complete
// pull the name apart
final char SEP = ';';
String pkgPart = ""; //$NON-NLS-1$
String pathPart = ""; //$NON-NLS-1$
String namePart = ""; //$NON-NLS-1$
int at = name.indexOf(SEP);
if (at > -1)
{
// have at least 2 parts to name
int nextAt = name.indexOf(SEP, at+1);
if (nextAt > -1)
{
// have 3 parts
pathPart = name.substring(0, at);
pkgPart = name.substring(at+1, nextAt);
namePart = name.substring(nextAt+1);
}
else
{
// 2 parts means no package.
pathPart = name.substring(0, at);
namePart = name.substring(at+1);
}
}
else
{
// should not be here....
// trim by last slash
at = name.lastIndexOf('/');
if (at > -1)
{
// cheat by looking for dirname "mx" in path
int mx = name.lastIndexOf("/mx/"); //$NON-NLS-1$
if (mx > -1)
{
pathPart = name.substring(0, mx);
pkgPart = name.substring(mx+1, at);
}
else
{
pathPart = name.substring(0, at);
}
namePart = name.substring(at+1);
}
else
{
pathPart = "."; //$NON-NLS-1$
namePart = name;
}
}
String script = null;
try
{
// now try to locate the thing on disk or in a swd.
Charset realEncoding = null;
Charset bomEncoding = null;
InputStream in = locateScriptFile(pathPart, pkgPart, namePart);
if (in != null)
{
try
{
// Read the file using the appropriate encoding, based on
// the BOM (if there is a BOM) or the default charset for
// the system (if there isn't a BOM)
BufferedInputStream bis = new BufferedInputStream( in );
bomEncoding = getEncodingFromBOM(bis);
script = pullInSource(bis, bomEncoding);
// If the file is an XML file with an <?xml> directive,
// it may specify a different directive
realEncoding = getEncodingFromXMLDirective(script);
}
finally
{
try { in.close(); } catch (IOException e) {}
}
}
// If we found an <?xml> directive with a specified encoding, and
// it doesn't match the encoding we used to read the file initially,
// start over.
if (realEncoding != null && !realEncoding.equals(bomEncoding))
{
in = locateScriptFile(pathPart, pkgPart, namePart);
if (in != null)
{
try
{
// Read the file using the real encoding, based on the
// <?xml...> directive
BufferedInputStream bis = new BufferedInputStream( in );
getEncodingFromBOM(bis);
script = pullInSource(bis, realEncoding);
}
finally
{
try { in.close(); } catch (IOException e) {}
}
}
}
}
catch(FileNotFoundException fnf)
{
fnf.printStackTrace(); // shouldn't really happen
}
return script;
}
/**
* Logic to poke around on disk in order to find the given
* filename. We look under the mattress and all other possible
* places for the silly thing. We always try locating
* the file directly first, if that fails then we hunt out
* the swd.
*/
InputStream locateScriptFile(String path, String pkg, String name) throws FileNotFoundException
{
if (m_sourceLocator != null)
{
m_sourceLocatorChangeCount = m_sourceLocator.getChangeCount();
InputStream is = m_sourceLocator.locateSource(path, pkg, name);
if (is != null)
return is;
}
// convert slashes first
path = path.replace('/', File.separatorChar);
pkg = pkg.replace('/', File.separatorChar);
File f;
// use a package base directory if it exists
if (path.length() > 0)
{
try
{
String pkgAndName = ""; //$NON-NLS-1$
if (pkg.length() > 0) // have to do this so we don't end up with just "/filename"
pkgAndName += pkg + File.separatorChar;
pkgAndName += name;
f = new File(path, pkgAndName);
if (f.exists())
return new FileInputStream(f);
}
catch(NullPointerException npe)
{
// skip it.
}
}
// try the current directory plus package
if (pkg.length() > 0) // new File("", foo) looks in root directory!
{
f = new File(pkg, name);
if (f.exists())
return new FileInputStream(f);
}
// look in the current directory without the package
f = new File(name);
if (f.exists())
return new FileInputStream(f);
// @todo try to pry open a swd file...
return null;
}
/**
* See if this document starts with a BOM and try to figure
* out an encoding from it.
* @param bis BufferedInputStream for document (so that we can reset the stream
* if we establish that the first characters aren't a BOM)
* @return CharSet from BOM (or system default / null)
*/
private Charset getEncodingFromBOM(BufferedInputStream bis)
{
Charset bomEncoding = null;
bis.mark(3);
String bomEncodingString;
try
{
bomEncodingString = FileUtils.consumeBOM(bis, null);
}
catch (IOException e)
{
bomEncodingString = System.getProperty("file.encoding"); //$NON-NLS-1$
}
bomEncoding = Charset.forName(bomEncodingString);
return bomEncoding;
}
/**
* Syntax for an <?xml ...> directive with an encoding (used by getEncodingFromXMLDirective)
*/
private static final Pattern sXMLDeclarationPattern = Pattern.compile("^<\\?xml[^>]*encoding\\s*=\\s*(\"([^\"]*)\"|'([^']*)')"); //$NON-NLS-1$
/**
* See if this document starts with an <?xml ...> directive and
* try to figure out an encoding from it.
* @param entireSource source of document
* @return specified Charset (or null)
*/
private Charset getEncodingFromXMLDirective(String entireSource)
{
String encoding = null;
Matcher xmlDeclarationMatcher = sXMLDeclarationPattern.matcher(entireSource);
if (xmlDeclarationMatcher.find())
{
encoding = xmlDeclarationMatcher.group(2);
if (encoding == null)
encoding = xmlDeclarationMatcher.group(3);
try
{
return Charset.forName(encoding);
}
catch (IllegalArgumentException e)
{}
}
return null;
}
/**
* Given an input stream containing source file contents, read in each line
* @param in stream of source file contents (with BOM removed)
* @param encoding encoding to use (based on BOM, system default, or <?xml...> directive
* if this is null, the system default will be used)
* @return source file contents (as String)
*/
String pullInSource(InputStream in, Charset encoding)
{
String script = ""; //$NON-NLS-1$
BufferedReader f = null;
try
{
StringBuilder sb = new StringBuilder();
Reader reader = null;
if (encoding == null)
reader = new InputStreamReader(in);
else
reader = new InputStreamReader(in, encoding);
f = new BufferedReader(reader);
String line;
while((line = f.readLine()) != null)
{
sb.append(line);
sb.append('\n');
}
script = sb.toString();
}
catch (IOException e)
{
e.printStackTrace(); //To change body of catch statement use File | Settings | File Templates.
}
return script;
}
/** for debugging */
@Override
public String toString()
{
return getFullPath();
}
/**
* Given a filename of the form "basepath;package;filename", return an
* array of 3 strings, one for each segment.
* @param name a string which *may* be of the form "basepath;package;filename"
* @return an array of 3 strings for the three pieces; or, if 'name' is
* not of expected form, returns null
*/
private static class NameParser
{
private String fOriginalName;
private String fBasePath;
private String fPackage;
private String fFilename;
private String fRecombinedName;
public NameParser(String name)
{
fOriginalName = name;
/* is it of "basepath;package;filename" format? */
int semicolonCount = 0;
int i = 0;
int firstSemi = -1;
int lastSemi = -1;
while ( (i = name.indexOf(';', i)) >= 0 )
{
++semicolonCount;
if (firstSemi == -1)
firstSemi = i;
lastSemi = i;
++i;
}
if (semicolonCount == 2)
{
fBasePath = name.substring(0, firstSemi);
fPackage = name.substring(firstSemi+1, lastSemi);
fFilename = name.substring(lastSemi+1);
}
}
public boolean isPathPackageAndFilename()
{
return (fBasePath != null);
}
public String getOriginalName()
{
return fOriginalName;
}
public String getBasePath()
{
return fBasePath;
}
public String getFilename()
{
return fFilename;
}
public String getPackage()
{
return fPackage;
}
/**
* Returns a "recombined" form of the original name.
*
* For filenames which came in in the form "basepath;package;filename",
* the recombined name is the original name with the semicolons replaced
* by platform-appropriate slash characters. For any other type of original
* name, the recombined name is the same as the incoming name.
*/
public String recombine()
{
if (fRecombinedName == null)
{
if (isPathPackageAndFilename())
{
char slashChar;
if (fOriginalName.indexOf('\\') != -1)
slashChar = '\\';
else
slashChar = '/';
fRecombinedName = fOriginalName.replaceAll(";;", ";").replace(';', slashChar); //$NON-NLS-1$ //$NON-NLS-2$
}
else
{
fRecombinedName = fOriginalName;
}
}
return fRecombinedName;
}
}
}