/**
* This file Copyright (c) 2005-2008 Aptana, Inc. This program is
* dual-licensed under both the Aptana Public License and the GNU General
* Public license. You may elect to use one or the other of these licenses.
*
* This program is distributed in the hope that it will be useful, but
* AS-IS and WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE, TITLE, or
* NONINFRINGEMENT. Redistribution, except as permitted by whichever of
* the GPL or APL you select, is prohibited.
*
* 1. For the GPL license (GPL), you can redistribute and/or modify this
* program under the terms of the GNU General Public License,
* Version 3, as published by the Free Software Foundation. You should
* have received a copy of the GNU General Public License, Version 3 along
* with this program; if not, write to the Free Software Foundation, Inc., 51
* Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Aptana provides a special exception to allow redistribution of this file
* with certain other free and open source software ("FOSS") code and certain additional terms
* pursuant to Section 7 of the GPL. You may view the exception and these
* terms on the web at http://www.aptana.com/legal/gpl/.
*
* 2. For the Aptana Public License (APL), this program and the
* accompanying materials are made available under the terms of the APL
* v1.0 which accompanies this distribution, and is available at
* http://www.aptana.com/legal/apl/.
*
* You may view the GPL, Aptana's exception and additional terms, and the
* APL in the file titled license.html at the root of the corresponding
* plugin containing this source file.
*
* Any modifications to this file must keep this entire header intact.
*/
package com.aptana.ide.scripting;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.HashSet;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.TransformerFactoryConfigurationError;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import org.mozilla.javascript.Callable;
import org.mozilla.javascript.Context;
import org.mozilla.javascript.NativeJavaObject;
import org.mozilla.javascript.Script;
import org.mozilla.javascript.Scriptable;
import org.mozilla.javascript.ScriptableObject;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.Text;
import org.xml.sax.SAXException;
import com.aptana.ide.core.IdeLog;
import com.aptana.ide.core.StringUtils;
import com.aptana.ide.server.http.HttpContentTypes;
import com.aptana.ide.server.http.HttpServer;
import com.aptana.ide.server.resources.IHttpResource;
/**
* @author Kevin Lindsey
*/
public class ScriptingHttpResource implements IHttpResource
{
/*
* Fields
*/
private static String RUNAT_ATTR = "runat"; //$NON-NLS-1$
private static String SERVER = "server"; //$NON-NLS-1$
private static String SERVER_ONLY = "server-only"; //$NON-NLS-1$
private static String BOTH = "both"; //$NON-NLS-1$
private File _file;
private String _text;
private String _type;
private Document _document;
private ScriptingHttpServer _server;
/*
* Properties
*/
/**
* Try to retrieve the document node from a previous load of this page
*
* @return Document
*/
private Document getCachedDocument()
{
ScriptInfo info = this.getScriptInfo();
Document result = null;
if (info != null)
{
Object doc = info.getScope().get("document", info.getScope()); //$NON-NLS-1$
if (doc instanceof NativeJavaObject)
{
NativeJavaObject nativeObject = (NativeJavaObject) doc;
result = (Document) nativeObject.unwrap();
}
}
return result;
}
/**
* setCachedDocument
*/
private void setCachedDocument()
{
ScriptInfo info = this.getScriptInfo();
if (info != null)
{
Context.enter();
Scriptable global = info.getScope();
Object wrappedDocument = Context.javaToJS(this._document, global);
global.put("document", global, wrappedDocument); //$NON-NLS-1$
global.put("location", global, info.getFile().getAbsolutePath()); //$NON-NLS-1$
Context.exit();
}
}
/**
* @see com.aptana.ide.server.resources.IHttpResource#getContentLength()
*/
public long getContentLength()
{
long result;
if (this._text != null)
{
result = this._text.length();
}
else
{
result = this._file.length();
}
return result;
}
/**
* @see com.aptana.ide.server.resources.IHttpResource#getContentType()
*/
public String getContentType()
{
return this._type;
}
/**
* getScriptInfo
*
* @return ScriptInfo
*/
private ScriptInfo getScriptInfo()
{
Global global = this._server.getGlobal();
String id = global.getXrefId(this.getUri());
ScriptInfo result = null;
if (global.hasScriptInfo(id))
{
result = global.getScriptInfo(id);
}
return result;
}
/**
* Determines if this resource is one that we should process
*
* @return Returns true if this a resource we need to pre-process
*/
private boolean isScriptanaResource()
{
boolean result = false;
int fileExtIndex = this._file.getName().lastIndexOf('.');
if (fileExtIndex != -1)
{
String fileExtension = this._file.getName().substring(fileExtIndex);
this._type = HttpContentTypes.getContentType(fileExtension);
if (this._type.equals("application/xhtml+xml")) //$NON-NLS-1$
{
this._type = "text/html"; //$NON-NLS-1$
result = true;
}
}
return result;
}
/**
* Get the URI for this resource
*
* @return String
*/
public String getUri()
{
String result = StringUtils.EMPTY;
try
{
result = this._file.getCanonicalPath();
}
catch (IOException e)
{
IdeLog.logError(ScriptingPlugin.getDefault(), Messages.ScriptingHttpResource_Error, e);
}
return result;
}
/*
* Constructors
*/
/**
* Create a new instance of FileHttpResource
*
* @param file
*/
public ScriptingHttpResource(File file)
{
this._file = file;
this._text = null;
this._type = "text/plain"; //$NON-NLS-1$
}
/*
* Methods
*/
/**
* getContentInputStream
*
* @param server
* @return InputStream
*/
public InputStream getContentInputStream(HttpServer server)
{
InputStream result = null;
// save reference to server
this._server = (ScriptingHttpServer) server;
try
{
if (this.isScriptanaResource())
{
// init document
this._document = this.getCachedDocument();
if (this._document == null)
{
// create a new scripting environment and load the page into it
this.loadXHTML();
}
else
{
ScriptInfo info = this.getScriptInfo();
if (info.needsRefresh())
{
// remove the stale scripting environment for this file
this._server.removeScriptEnvironment(this.getUri());
// create a new scripting environment and load the page into it
this.loadXHTML();
}
else
{
Script[] scripts = info.getScripts();
// exec
Context cx = Context.enter();
for (int i = 0; i < scripts.length; i++)
{
scripts[i].exec(cx, info.getScope());
}
Context.exit();
}
}
// save buffer contents so we can return the size in getContentLength
this._text = this.nodeToString(this._document);
// create stream
result = new ByteArrayInputStream(this._text.getBytes("UTF-8")); //$NON-NLS-1$
}
else
{
// create stream from file
result = new FileInputStream(this._file);
}
}
catch (UnsupportedEncodingException e)
{
String message = StringUtils.format(Messages.ScriptingHttpResource_Processing_Error, this.getUri());
IdeLog.logError(ScriptingPlugin.getDefault(), message, e);
}
catch (FileNotFoundException e)
{
String message = StringUtils.format(Messages.ScriptingHttpResource_Processing_Error, this.getUri());
IdeLog.logError(ScriptingPlugin.getDefault(), message, e);
}
catch (TransformerFactoryConfigurationError e)
{
String message = StringUtils.format(Messages.ScriptingHttpResource_Processing_Error, this.getUri());
IdeLog.logError(ScriptingPlugin.getDefault(), message, e);
}
return result;
}
/**
* Process an XHTML file
*/
private void loadXHTML()
{
this._server.createScriptEnvironment(this.getUri());
try
{
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
// parse XML to create our document element
this._document = builder.parse(this._file);
// save a reference to this document in the scripting environment
this.setCachedDocument();
// process <script> blocks
this.processScriptElements();
}
catch (ParserConfigurationException e)
{
String message = StringUtils.format(Messages.ScriptingHttpResource_Processing_Error, this.getUri());
IdeLog.logError(ScriptingPlugin.getDefault(), message, e);
}
catch (SAXException e)
{
String message = StringUtils.format(Messages.ScriptingHttpResource_Processing_Error, this.getUri());
IdeLog.logError(ScriptingPlugin.getDefault(), message, e);
}
catch (IOException e)
{
String message = StringUtils.format(Messages.ScriptingHttpResource_Processing_Error, this.getUri());
IdeLog.logError(ScriptingPlugin.getDefault(), message, e);
}
catch (TransformerFactoryConfigurationError e)
{
String message = StringUtils.format(Messages.ScriptingHttpResource_Processing_Error, this.getUri());
IdeLog.logError(ScriptingPlugin.getDefault(), message, e);
}
}
/**
* getNodeText
*
* @param node
* @return String
*/
private String nodeToString(Node node)
{
String result = StringUtils.EMPTY;
try
{
DOMSource source = new DOMSource(node);
StringWriter writer = new StringWriter();
StreamResult streamResult = new StreamResult(writer);
Transformer xformer = TransformerFactory.newInstance().newTransformer();
// output to buffer
xformer.transform(source, streamResult);
// save buffer contents so we can return the size later
result = writer.toString();
}
catch (TransformerConfigurationException e)
{
String message = StringUtils.format(Messages.ScriptingHttpResource_Processing_Error, this.getUri());
IdeLog.logError(ScriptingPlugin.getDefault(), message, e);
}
catch (TransformerException e)
{
String message = StringUtils.format(Messages.ScriptingHttpResource_Processing_Error, this.getUri());
IdeLog.logError(ScriptingPlugin.getDefault(), message, e);
}
return result;
}
/**
* processScriptElements
*/
private void processScriptElements()
{
// grab all script elements
NodeList scripts = this._document.getElementsByTagName("script"); //$NON-NLS-1$
Element[] scriptElements = new Element[scripts.getLength()];
ArrayList serverScripts = new ArrayList();
ArrayList serverOnlyScripts = new ArrayList();
ArrayList clientServerScripts = new ArrayList();
// make an array of script elements since NodeList doesn't act properly when we remove its children from the DOM
for (int i = 0; i < scripts.getLength(); i++)
{
scriptElements[i] = (Element) scripts.item(i);
}
// sort server-side scripts into categories for easy processing
for (int i = 0; i < scriptElements.length; i++)
{
Element script = scriptElements[i];
if (script.hasAttribute(RUNAT_ATTR))
{
String runAt = script.getAttribute(RUNAT_ATTR);
if (runAt.equals(SERVER))
{
serverScripts.add(script);
}
else if (runAt.equals(SERVER_ONLY))
{
serverOnlyScripts.add(script);
}
else if (runAt.equals(BOTH))
{
clientServerScripts.add(script);
}
}
}
HashSet baselineFunctions = this.getFunctionNames();
HashSet bothFunctions = new HashSet();
HashSet serverFunctions = new HashSet();
// process scripts that have runat="both" and remove attribute
for (int i = 0; i < clientServerScripts.size(); i++)
{
Element script = (Element) clientServerScripts.get(i);
this.runScript(script);
script.removeAttribute(RUNAT_ATTR);
bothFunctions.addAll(this.getFunctionNames());
}
// process and remove scripts that have runat="server"
for (int i = 0; i < serverScripts.size(); i++)
{
Element script = (Element) serverScripts.get(i);
this.runScript(script);
script.getParentNode().removeChild(script);
serverFunctions.addAll(this.getFunctionNames());
}
// process and remove scripts that have runat="server-only"
for (int i = 0; i < serverOnlyScripts.size(); i++)
{
Element script = (Element) serverOnlyScripts.get(i);
this.runScript(script);
script.getParentNode().removeChild(script);
// serverFunctions.addAll(this.getFunctionNames());
}
// remove baseline functions
bothFunctions.removeAll(baselineFunctions);
serverFunctions.removeAll(baselineFunctions);
// remove "both" functions from "server"
serverFunctions.removeAll(bothFunctions);
// create <script> element with wrapped functions
Element wrapper = this.createWrappers((String[]) serverFunctions.toArray(new String[0]));
// add <script> to tree
if (wrapper != null)
{
// create a script element for our library
Element script = this._document.createElement("script"); //$NON-NLS-1$
script.setAttribute("type", "text/javascript"); //$NON-NLS-1$ //$NON-NLS-2$
script.setAttribute("src", "/aptana/libs/xmlhttp.js"); //$NON-NLS-1$ //$NON-NLS-2$
// find the first head element or create one
NodeList heads = this._document.getElementsByTagName("head"); //$NON-NLS-1$
Element head;
if (heads.getLength() > 0)
{
head = (Element) heads.item(0);
}
else
{
Element html = this._document.getDocumentElement();
head = this._document.createElement("head"); //$NON-NLS-1$
if (html.hasChildNodes())
{
html.insertBefore(head, html.getFirstChild());
}
else
{
html.appendChild(head);
}
}
// add script elements as first two children of head
if (head.hasChildNodes())
{
head.insertBefore(wrapper, head.getFirstChild());
}
else
{
head.appendChild(wrapper);
}
// insert wrappers
head.insertBefore(script, wrapper);
}
}
/**
* createWrappers
*/
private Element createWrappers(String[] names)
{
Element result = null;
if (names.length > 0)
{
StringBuffer buffer = new StringBuffer();
for (int i = 0; i < names.length; i++)
{
buffer.append("\n"); //$NON-NLS-1$
buffer.append(this.createFunctionWrapper(names[i]));
}
buffer.append("\n"); //$NON-NLS-1$
String genCode = buffer.toString();
result = this._document.createElement("script"); //$NON-NLS-1$
Text wrapperCode = this._document.createTextNode(genCode);
result.setAttribute("type", "text/javascript"); //$NON-NLS-1$ //$NON-NLS-2$
result.appendChild(wrapperCode);
}
return result;
}
/**
* @param name
* @return String
*/
private String createFunctionWrapper(String name)
{
StringBuffer buffer = new StringBuffer();
buffer.append("function ").append(name).append("() {").append(" "); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
buffer.append("return ___invokeFunction.call(null, \"").append(name).append("\", arguments);").append(" "); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
buffer.append("}"); //$NON-NLS-1$
return buffer.toString();
}
/**
* getServerFunctionNames
*
* @return HashSet
*/
private HashSet getFunctionNames()
{
ScriptInfo info = this.getScriptInfo();
ScriptableObject global = (ScriptableObject) info.getScope();
Object[] ids = global.getIds();
HashSet names = new HashSet();
for (int i = 0; i < ids.length; i++)
{
Object idObject = ids[i];
if (idObject instanceof String)
{
String id = (String) idObject;
int attrs = global.getAttributes(id);
boolean readonly = (attrs & ScriptableObject.READONLY) == ScriptableObject.READONLY;
if (readonly == false)
{
Object value = global.get(id, global);
if (value instanceof Callable)
{
names.add(id);
}
}
}
}
return names;
}
/**
* runScript
*
* @param script
*/
private void runScript(Element script)
{
String code = null;
if (script.hasAttribute("src")) //$NON-NLS-1$
{
String filename = script.getAttribute("src"); //$NON-NLS-1$
File file = new File(filename);
if (file.exists() == false)
{
String parentDirectory = this._file.getParent();
String candidate = parentDirectory + File.separator + filename;
file = new File(candidate);
}
if (file.exists() == false && filename.startsWith("/")) //$NON-NLS-1$
{
String rootServerPath = _server.getRootPath();
String candidate = rootServerPath + filename;
file = new File(candidate);
}
if (file.exists())
{
try
{
FileInputStream input = new FileInputStream(file);
code = FileUtilities.getStreamText(input);
}
catch (FileNotFoundException e)
{
IdeLog.logError(ScriptingPlugin.getDefault(), Messages.ScriptingHttpResource_Error, e);
}
}
else
{
String message = StringUtils.format(Messages.ScriptingHttpResource_File_Does_Not_Exist, filename);
IdeLog.logError(ScriptingPlugin.getDefault(), message);
}
}
else
{
StringBuffer codePieces = new StringBuffer();
Node child = script.getFirstChild();
while (child != null)
{
codePieces.append(child.getNodeValue());
child = child.getNextSibling();
}
code = codePieces.toString();
}
if (code != null && code.length() > 0)
{
this.runScript(code);
}
}
/**
* runScript
*
* @param script
*/
private void runScript(String script)
{
this._server.include(this.getUri(), script);
}
}