/*
* file.js
* Objective-J
*
* Created by Francisco Tolmasky.
* Copyright 2008, 280 North, Inc.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
// Notes: requests may return immediately (i.e. in IE when a file has been cached). Don't assume
// One line of code thus follows another, it may call the callback first!
// FIXME: If we ever have bundles that replace only some files, we will definitely run into problems.
// Look inside bundleResponseCallback.
var OBJJ_PLATFORMS = PLATFORMS;
#define DIRECTORY(aPath) (aPath).substr(0, (aPath).lastIndexOf('/') + 1)
var OBJJFileNotFoundException = "OBJJFileNotFoundException",
OBJJExecutableNotFoundException = "OBJJExecutableNotFoundException";
var objj_files = { },
objj_bundles = { },
objj_bundlesForClass = { },
objj_searches = { };
var OBJJ_NO_FILE = {};
if (typeof OBJJ_INCLUDE_PATHS === "undefined")
OBJJ_INCLUDE_PATHS = ["Frameworks", "SomethingElse"];
var OBJJ_BASE_URI = "";
IF (OPERA)
var DOMBaseElement = document.getElementsByTagName("base")[0];
if (DOMBaseElement)
OBJJ_BASE_URI = DIRECTORY(DOMBaseElement.getAttribute('href'));
ENDIF
function objj_file()
{
this.path = NULL;
this.bundle = NULL;
this.included = NO;
this.contents = NULL;
this.fragments = NULL;
}
function objj_bundle()
{
this.path = NULL;
this.info = NULL;
this.__address = _objj_generateObjectHash();
}
function objj_getBundleWithPath(aPath)
{
return objj_bundles[aPath];
}
function objj_setBundleForPath(aPath, aBundle)
{
objj_bundles[aPath] = aBundle;
}
function objj_bundleForClass(aClass)
{
return objj_bundlesForClass[aClass.name];
}
function objj_addClassForBundle(aClass, aBundle)
{
objj_bundlesForClass[aClass.name] = aBundle;
}
function objj_request_file(aFilePath, shouldSearchLocally, aCallback)
{
new objj_search(aFilePath, shouldSearchLocally, aCallback).attemptNextSearchPath();
}
var objj_search = function(aFilePath, shouldSearchLocally, aCallback)
{
this.filePath = aFilePath;
this.bundle = NULL;
this.bundleObservers = [];
this.searchPath = NULL;
this.searchedPaths = [];
this.includePathsIndex = shouldSearchLocally ? -1 : 0;
this.searchRequest = NULL;
this.didCompleteCallback = aCallback;
}
objj_search.prototype.nextSearchPath = function()
{
// FIXME: Path searching should be more complex (go up directories, etc).
var path = objj_standardize_path((this.includePathsIndex == -1 ? "" : OBJJ_INCLUDE_PATHS[this.includePathsIndex] + '/') + this.filePath);
++this.includePathsIndex;
return path;
}
objj_search.prototype.attemptNextSearchPath = function()
{
var searchPath = this.nextSearchPath(),
file = objj_files[searchPath];
objj_alert("Will attempt to find " + this.filePath + " at " + searchPath);
// If a file for this search path already exists, then it has already been downloaded.
// If there is a callback, we can just call it, if not, we can simply return.
if (file)
{
objj_alert("The file request at " + this.filePath + " has already been downloaded at " + searchPath);
// FIXME: Do we need this for everything?
#if RHINO
var index = 0,
count = this.searchedPaths.length;
for (; index < count; ++index)
objj_files[this.searchedPaths[index]] = file;
#endif
if (this.didCompleteCallback)
this.didCompleteCallback(file);
return;
}
var existingSearch = objj_searches[searchPath];
// If there is already an ongoing search for this search path, then we can let it find
// the file for us. Make sure to assign it our callback, if we have one, since only
// one search can have a callback at a time.
if (existingSearch)
{
if (this.didCompleteCallback)
existingSearch.didCompleteCallback = this.didCompleteCallback;
return;
}
// At this point we have a legitimate search, so we should take note of this search path
// in our search paths.
this.searchedPaths.push(this.searchPath = searchPath);
// Before we kick off our ajax requests, see whether this directory already has
// a bundle associated with it.
var infoPath = objj_standardize_path(DIRECTORY(searchPath) + "Info.plist"),
bundle = objj_bundles[infoPath];
// If there is, then simply look for the file in question.
if (bundle)
{
this.bundle = bundle;
this.request(searchPath, this.didReceiveSearchResponse);
}
else
{
// If there isn't a bundle associated with this directory, then we also have to
// search for an Info.plist.
var existingBundleSearch = objj_searches[infoPath];
// Again, if a search already exists for this bundle, then add ourselves to the list
// of observers for when this bundle is either found or created.
if (existingBundleSearch)
{
--this.includePathsIndex;
this.searchedPaths.pop();
if (this.searchedPaths.length)
this.searchPath = this.searchedPaths[this.searchedPaths.length - 1];
else
this.searchPath = NULL;
existingBundleSearch.bundleObservers.push(this);
return;
}
// If not, then look for it.
else
{
this.bundleObservers.push(this);
this.request(infoPath, this.didReceiveBundleResponse);
// Requests may return immediately, so by this point, we may have already gotten the bundle and replaced the file.
if (!this.searchReplaced)
this.searchRequest = this.request(searchPath, this.didReceiveSearchResponse);
}
}
}
IF (ACTIVE_X)
objj_search.responseCallbackLock = NO;
objj_search.responseCallbackQueue = [];
objj_search.removeResponseCallbackForFilePath = function(aFilePath)
{
var queue = objj_search.responseCallbackQueue,
index = queue.length;
while (index--)
if (queue[index][3] == aFilePath)
{
queue.splice(index, 1);
return;
}
}
objj_search.serializeResponseCallback = function(aMethod, aSearch, aResponse, aFilePath)
{
var queue = objj_search.responseCallbackQueue;
queue.push([aMethod, aSearch, aResponse, aFilePath]);
if (objj_search.responseCallbackLock)
return;
objj_search.responseCallbackLock = YES;
while (queue.length)
{
var callback = queue[0];
queue.splice(0, 1);
callback[0].apply(callback[1], [callback[2]]);
}
objj_search.responseCallbackLock = NO;
}
ENDIF
objj_search.prototype.request = function(aFilePath, aMethod)
{
var search = this,
isPlist = aFilePath.substr(aFilePath.length - 6, 6) == ".plist",
request = objj_request_xmlhttp(),
response = objj_response_xmlhttp();
response.filePath = aFilePath;
request.onreadystatechange = function()
{
if (request.readyState == 4)
{
if (response.success = (request.status != 404 && request.responseText && request.responseText.length) ? YES : NO)
{
if (window.files_total)
{
if (!window.files_loaded)
window.files_loaded = 0;
window.files_loaded += request.responseText.length;
if (window.update_progress)
window.update_progress(window.files_loaded / window.files_total);
}
if (isPlist)
response.xml = objj_standardize_xml(request);
else
response.text = request.responseText;
}
if (ACTIVE_X)
objj_search.serializeResponseCallback(aMethod, search, response, aFilePath);
else
aMethod.apply(search, [response]);
}
}
objj_searches[aFilePath] = this;
if (request.overrideMimeType && isPlist)
request.overrideMimeType('text/xml');
if (OPERA && aFilePath.charAt(0) != '/')
aFilePath = OBJJ_BASE_URI + aFilePath;
try
{
// unclear whether plusses are reserved in the URI path
//request.open("GET", aFilePath.replace(/\+/g, "%2B"), YES);
request.open("GET", aFilePath, YES);
request.send("");
}
catch (anException)
{
response.success = NO;
if (ACTIVE_X)
objj_search.serializeResponseCallback(aMethod, search, response, aFilePath);
else
aMethod.apply(search, [response]);
}
return request;
}
objj_search.prototype.didReceiveSearchResponse = function(aResponse)
{
// If we do not have a bundle yet, we cannot appropriately
// handle this response, so wait.
if (!this.bundle)
{
this.cachedSearchResponse = aResponse;
return;
}
if (aResponse.success)
{
file = new objj_file();
file.path = aResponse.filePath;
file.bundle = this.bundle
file.contents = aResponse.text;
this.complete(file);
}
else if (this.includePathsIndex < OBJJ_INCLUDE_PATHS.length)
{
// Clear out the bundle since it will no longer be valid.
// FIXME: FIXME: FIXME: WARNING: Wouldn't it be cleaner to just always do this in attemptNextSearchPath?
this.bundle = NULL;
this.attemptNextSearchPath();
}
else
objj_exception_throw(new objj_exception(OBJJFileNotFoundException, "*** Could not locate file named \"" + this.filePath + "\" in search paths."));
}
objj_search.prototype.didReceiveBundleResponse = function(aResponse)
{
var bundle = new objj_bundle();
bundle.path = aResponse.filePath;
if (aResponse.success)
bundle.info = CPPropertyListCreateFromXMLData(aResponse.xml);
else
bundle.info = new objj_dictionary();
objj_bundles[aResponse.filePath] = bundle;
var executablePath = dictionary_getValue(bundle.info, "CPBundleExecutable");
if (executablePath)
{
var platform = NULL,
platforms = dictionary_getValue(bundle.info, "CPBundlePlatforms"),
index = 0,
count = OBJJ_PLATFORMS.length,
innerCount = platforms.length;
// Ugh, no indexOf, no objects-in-common.
for(; index < count; ++index)
{
var innerIndex = 0,
currentPlatform = OBJJ_PLATFORMS[index];
for (; innerIndex < innerCount; ++innerIndex)
if(currentPlatform === platforms[innerIndex])
{
platform = currentPlatform;
break;
}
}
executablePath = platform + ".platform/" + executablePath;
this.request(DIRECTORY(aResponse.filePath) + executablePath, this.didReceiveExecutableResponse);
// FIXME: Is this the right approach?
// Request the compiled file regardless of whether our current inquiry
var directory = DIRECTORY(aResponse.filePath),
replacedFiles = dictionary_getValue(bundle.info, "CPBundleReplacedFiles"),
index = 0,
count = replacedFiles.length;
for (; index < count; ++index)
{
// Halt any forward searches of these files from taking place.
objj_searches[directory + replacedFiles[index]] = this;
if (directory + replacedFiles[index] == this.searchPath)
{
this.searchReplaced = YES;
if (!this.cachedSearchResponse && this.searchRequest)
this.searchRequest.abort();
if (ACTIVE_X)
objj_search.removeResponseCallbackForFilePath(this.searchPath);
}
}
}
this.bundle = bundle;
var observers = this.bundleObservers,
index = 0,
count = observers.length;
for(; index < count; ++index)
{
var observer = observers[index];
// Force the observer to just attempt the path all over again,
// since it may have been replaced by the executable.
// FIXME: we should be just reattempting the ones that collided.
if (observer != this)
observer.attemptNextSearchPath();
// If we have a cached response and the search has not been
// replaced by the executable, then let it proceed.
else if (this.cachedSearchResponse && !this.searchReplaced)
this.didReceiveSearchResponse(this.cachedSearchResponse);
}
this.bundleObservers = [];
}
objj_search.prototype.didReceiveExecutableResponse = function(aResponse)
{
if (!aResponse.success)
objj_exception_throw(new objj_exception(OBJJExecutableNotFoundException, "*** The specified executable could not be located at \"" + this.filePath + "\"."));
var files = objj_decompile(aResponse.text, this.bundle),
index = 0,
count = files.length,
length = this.filePath.length;
for (; index < count; ++index)
{
var file = files[index],
path = file.path;
if (this.filePath == path.substr(path.length - length))
this.complete(file);
else
objj_files[path] = file;
}
}
objj_search.prototype.complete = function(aFile)
{
var index = 0,
count = this.searchedPaths.length;
for (; index < count; ++index)
{
objj_files[this.searchedPaths[index]] = aFile;
//FIXME: uncomment this:
//delete objj_inquiries[anInquiry.searchedPaths[index]];
}
if (this.didCompleteCallback)
this.didCompleteCallback(aFile);
}
// objj_standardize_path
//
// Standardizes the input path by removing extrenenous components and resolving
// references to parent directories.
function objj_standardize_path(aPath)
{
if (aPath.indexOf("/./") != -1 && aPath.indexOf("//") != -1 && aPath.indexOf("/../") != -1)
return aPath;
var index = 0,
components = aPath.split('/');
for(;index < components.length; ++index)
if(components[index] == "..")
{
components.splice(index - 1, 2);
index -= 2;
}
else if(index != 0 && !components[index].length || components[index] == '.' || components[index] == "..")
components.splice(index--, 1);
return components.join('/');
}
IF (ACTIVE_X)
var objj_standardize_xml = function(aRequest)
{
var XMLData = new ActiveXObject("Microsoft.XMLDOM");
XMLData.loadXML(aRequest.responseText.substr(aRequest.responseText.indexOf(".dtd\">") + 6));
return XMLData;
}
ELSE
var objj_standardize_xml = function(aRequest)
{
return aRequest.responseXML;
}
ENDIF
function objj_response_xmlhttp()
{
return new Object;
}
// objj_request_xmlhttp()
//
// To be used as a browser-independent implementation of XMLHttpRequest.
// Currently known to support Safari, Firefox, IE 6/7, and Opera.
//
// Throws a primitive exception if XMLHttpRequests are not supported on
// the given browser.
IF (NATIVE_XMLHTTPREQUEST)
var objj_request_xmlhttp = function()
{
return new XMLHttpRequest();
}
ELIF (ACTIVE_X)
// DON'T try 4.0 and 5.0. Should we be trying anything other than 3.0 and 6.0?
// http://blogs.msdn.com/xmlteam/archive/2006/10/23/using-the-right-version-of-msxml-in-internet-explorer.aspx
var MSXML_XMLHTTP_OBJECTS = [ "Microsoft.XMLHTTP", "Msxml2.XMLHTTP", "Msxml2.XMLHTTP.3.0", "Msxml2.XMLHTTP.6.0" ],//"Msxml2.XMLHTTP.4.0", "Msxml2.XMLHTTP.5.0" ],
index = MSXML_XMLHTTP_OBJECTS.length;
while (index--)
{
try
{
new ActiveXObject(MSXML_XMLHTTP_OBJECTS[index]);
break;
}
catch (anException)
{
}
}
var MSXML_XMLHTTP = MSXML_XMLHTTP_OBJECTS[index];
delete index;
delete MSXML_XMLHTTP_OBJECTS;
var objj_request_xmlhttp = function()
{
return new ActiveXObject(MSXML_XMLHTTP);
}
ENDIF
var OBJJUnrecognizedFormatException = "OBJJUnrecognizedFormatException";
var STATIC_MAGIC_NUMBER = "@STATIC",
MARKER_PATH = "p",
MARKER_CODE = "c",
MARKER_BUNDLE = "b",
MARKER_TEXT = "t",
MARKER_IMPORT_STD = 'I',
MARKER_IMPORT_LOCAL = 'i';
var STATIC_EXTENSION = "sj";
function objj_decompile(aString, bundle)
{
var stream = new objj_markedStream(aString);
if (stream.magicNumber() != STATIC_MAGIC_NUMBER)
objj_exception_throw(new objj_exception(OBJJUnrecognizedFormatException, "*** Could not recognize executable code format."));
if (stream.version() != 1.0)
objj_exception_throw(new objj_exception(OBJJUnrecognizedFormatException, "*** Could not recognize executable code format."));
var file = NULL,
files = [],
marker;
while (marker = stream.getMarker())
{
var text = stream.getString();
switch (marker)
{
case MARKER_PATH: if (file && file.contents && file.path === file.bundle.path)
file.bundle.info = CPPropertyListCreateWithData({string:file.contents});
file = new objj_file();
file.path = DIRECTORY(bundle.path) + text;
file.bundle = bundle;
file.fragments = [];
files.push(file);
objj_files[file.path] = file;
break;
case MARKER_BUNDLE: var bundlePath = DIRECTORY(bundle.path) + '/' + text;
file.bundle = objj_getBundleWithPath(bundlePath);
if (!file.bundle)
{
file.bundle = new objj_bundle();
file.bundle.path = bundlePath;
objj_setBundleForPath(file.bundle, bundlePath);
}
break;
case MARKER_TEXT: file.contents = text;
break;
case MARKER_CODE: file.fragments.push(fragment_create_code(text, bundle, file));
break;
case MARKER_IMPORT_STD: file.fragments.push(fragment_create_file(text, bundle, NO, file));
break;
case MARKER_IMPORT_LOCAL: file.fragments.push(fragment_create_file(text, bundle, YES, file));
break;
}
}
if (file && file.contents && file.path === file.bundle.path)
file.bundle.info = CPPropertyListCreateWithData({string:file.contents});
return files;
}