Posts tagged ‘rhino’

Przyglądam się dalej projektowi Rhino i „dopieszczam” system pluginów dostępnych z poziomu języka JavaScript, jednak ostatnio zaskoczyła mnie jedna rzecz. Po konwersji Listy korzystając z metody Context.javaToJS spodziewałem się, że dostęp do elementów będzie możliwy w taki sam sposób jak to jest w przypadku tablicy, czyli np.

mojaLista[0]

.
Niestety ku mojemu rozczarowaniu dostałem tylko wyjątkiem „po oczach” i koniec zabawy. Zacząłem się zastanawiać jak trudne byłoby dodanie takiej funkcjonalności i umożliwienie takich samych „czarów” jak w przypadku np. Expression Language (EL) znanego obytym z JSP.

Porozglądałem się trochę po kodzie i dodanie obsługi listy, wygląda dość prosto, aż tak prosto, że szukam jakiegoś chaczyka (ale o tym za chwilę), dopisałem klasę NativeJavaList, pozamieniałem co trzeba, żeby obsługiwała listę i voila:

/* -*- Mode: java; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
 *
 * ***** BEGIN LICENSE BLOCK *****
 * Version: MPL 1.1/GPL 2.0
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (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.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is Rhino code, released
 * May 6, 1999.
 *
 * The Initial Developer of the Original Code is
 * Netscape Communications Corporation.
 * Portions created by the Initial Developer are Copyright (C) 1997-1999
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *   Norris Boyd
 *   Igor Bukanov
 *   Frank Mitchell
 *   Mike Shaver
 *   Kemal Bayram
 *
 * Alternatively, the contents of this file may be used under the terms of
 * the GNU General Public License Version 2 or later (the "GPL"), in which
 * case the provisions of the GPL are applicable instead of those above. If
 * you wish to allow use of your version of this file only under the terms of
 * the GPL and not to allow others to use your version of this file under the
 * MPL, indicate your decision by deleting the provisions above and replacing
 * them with the notice and other provisions required by the GPL. If you do
 * not delete the provisions above, a recipient may use your version of this
 * file under either the MPL or the GPL.
 *
 * ***** END LICENSE BLOCK ***** */

package org.mozilla.javascript;

import java.util.List;

/**
 * This class reflects Java lists into the JavaScript environment.
 *
 * @author Marek Będkowski
 * @see NativeJavaClass
 * @see NativeJavaObject
 * @see NativeJavaPackage
 * @see NativeJavaArray
 */

public class NativeJavaList extends NativeJavaObject
{
    static final long serialVersionUID = -924022554283675333L;

    @Override
    public String getClassName() {
        return "JavaList";
    }

    public static NativeJavaList wrap(Scriptable scope, Object list) {
        return new NativeJavaList(scope, list);
    }

    @Override
    public Object unwrap() {
        return list;
    }

    public NativeJavaList(Scriptable scope, Object list) {
        super(scope, list, ScriptRuntime.ObjectClass);
        if( !(list instanceof java.util.List)){
            throw new RuntimeException("java.util.List expected");
        }
        this.list = (List<Object>)list;
        this.cls = list.getClass().getComponentType();
    }

    @Override
    public boolean has(String id, Scriptable start) {
        return id.equals("length") || super.has(id, start);
    }

    @Override
    public boolean has(int index, Scriptable start) {
        return 0 <= index && index < list.size();
    }

    @Override
    public Object get(String id, Scriptable start) {
        if (id.equals("length"))
            return new Integer(list.size());
        Object result = super.get(id, start);
        if (result == NOT_FOUND &&
            !ScriptableObject.hasProperty(getPrototype(), id))
        {
            throw Context.reportRuntimeError2(
                "msg.java.member.not.found", list.getClass().getName(), id);
        }
        return result;
    }

    @Override
    public Object get(int index, Scriptable start) {
        if (0 <= index && index < list.size()) {
            Context cx = Context.getContext();
            Object obj = list.get(index);
            return cx.getWrapFactory().wrap(cx, this, obj, cls);
        }
        return Undefined.instance;
    }

    @Override
    public void put(String id, Scriptable start, Object value) {
        // Ignore assignments to "length"--it's readonly.
    	// also make sure that nobody overrides list's interface
        if (!id.equals("length") || super.get(id, start) != null) {
            throw Context.reportRuntimeError1(
                "msg.property.or.method.not.accessible", id);
        }
    }

    @Override
    public void put(int index, Scriptable start, Object value) {
        if (0 <= index && index < list.size()) {
        	list.set(index, Context.jsToJava(value, cls));
        }
        else {
            throw Context.reportRuntimeError2(
                "msg.java.array.index.out.of.bounds", String.valueOf(index),
                String.valueOf(list.size() - 1));
        }
    }

    @Override
    public Object getDefaultValue(Class<?> hint) {
        if (hint == null || hint == ScriptRuntime.StringClass)
            return list.toString();
        if (hint == ScriptRuntime.BooleanClass)
            return Boolean.TRUE;
        if (hint == ScriptRuntime.NumberClass)
            return ScriptRuntime.NaNobj;
        return this;
    }

    @Override
    public Object[] getIds() {
        Object[] result = new Object[list.size()];
        int i = list.size();
        while (--i >= 0)
            result[i] = new Integer(i);
        return result;
    }

    @Override
    public boolean hasInstance(Scriptable value) {
        if (!(value instanceof Wrapper))
            return false;
        Object instance = ((Wrapper)value).unwrap();
        return cls.isInstance(instance);
    }

    @Override
    public Scriptable getPrototype() {
        if (prototype == null) {
            prototype =
                ScriptableObject.getClassPrototype(this.getParentScope(),
                                                   "Array");
        }
        return prototype;
    }

    List<Object> list;
    Class<?> cls;
}

Później „uświadomienie” obiektowi WrapFactory, że już obsługa listy jest i już można szaleć…

else if( obj instanceof java.util.List ){
        	return NativeJavaList.wrap(scope, obj);
        }

Jednak jak wspominałem na początku, sprawa wygląda podejrzanie prosto, że aż nasuwa się pytanie, dlaczego do tej pory nikt z teamu Rhino się tym nie zajął? Uważna lektura kodu, powyżej może podsunąć jedną z hipotez. Otóż typizacja kolekcji w Javie działa tylko podczas kompilacji, w czasie wykonywania informacja o typie jest wymazywana, w celu kompatybilności wstecznej. Dodając do tego, że JavaScript jest dynamicznie typowany mamy prośbę o kłopoty.

Lista może otrzymać ze strony JS’a, dowolny obiekt (java.lang.Object), żadnego błedu, ostrzeżenia nic – dopiero w momencie gdy będziemy próbowali pobrać naszego Inta, z tablicy Stringów dostaniemy „po łapkach”. Jak to mówią „nie ma nic za darmo”, ale moim zdaniem ułatwienie jest na tyle przyjemne, że postanowiłem pójść dalej tym tropem i obsłużyć jeszcze Map’y, oto wynik:

/* -*- Mode: java; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
 *
 * ***** BEGIN LICENSE BLOCK *****
 * Version: MPL 1.1/GPL 2.0
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (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.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is Rhino code, released
 * May 6, 1999.
 *
 * The Initial Developer of the Original Code is
 * Netscape Communications Corporation.
 * Portions created by the Initial Developer are Copyright (C) 1997-1999
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *   Norris Boyd
 *   Igor Bukanov
 *   Frank Mitchell
 *   Mike Shaver
 *   Kemal Bayram
 *
 * Alternatively, the contents of this file may be used under the terms of
 * the GNU General Public License Version 2 or later (the "GPL"), in which
 * case the provisions of the GPL are applicable instead of those above. If
 * you wish to allow use of your version of this file only under the terms of
 * the GPL and not to allow others to use your version of this file under the
 * MPL, indicate your decision by deleting the provisions above and replacing
 * them with the notice and other provisions required by the GPL. If you do
 * not delete the provisions above, a recipient may use your version of this
 * file under either the MPL or the GPL.
 *
 * ***** END LICENSE BLOCK ***** */

package org.mozilla.javascript;

import java.util.Iterator;
import java.util.Map;

/**
 * This class reflects Java Maps into the JavaScript environment.
 *
 * @author Marek Będkowski
 * @see NativeJavaClass
 * @see NativeJavaObject
 * @see NativeJavaPackage
 */

public class NativeJavaMap extends NativeJavaObject
{
    static final long serialVersionUID = -924022554283675333L;

    @Override
    public String getClassName() {
        return "JavaMap";
    }

    public static NativeJavaMap wrap(Scriptable scope, Object map) {
        return new NativeJavaMap(scope, map);
    }

    @Override
    public Object unwrap() {
        return map;
    }

    public NativeJavaMap(Scriptable scope, Object map) {
        super(scope, map, ScriptRuntime.ObjectClass);
        if( !(map instanceof java.util.Map)){
            throw new RuntimeException("java.util.Map expected");
        }
        this.map = (Map<Object,Object>)map;
        this.cls = this.map.keySet().toArray().getClass().getComponentType();
    }

    @Override
    public boolean has(String id, Scriptable start) {
        return id.equals("length") || super.has(id, start) || map.containsKey(id);
    }

    @Override
    public boolean has(int index, Scriptable start) {
        return map.containsKey(index);
    }

    @Override
    public Object get(String id, Scriptable start) {
        if (id.equals("length"))
            return new Integer(map.size());
        Object result = super.get(id, start);
        // instead of throwing error immediately try searching id as a map key
        if (result == NOT_FOUND &&
            !ScriptableObject.hasProperty(getPrototype(), id))
        {
        	if( !map.containsKey(id) ) {
	            throw Context.reportRuntimeError2(
	                "msg.java.member.not.found", map.getClass().getName(), id);
        	}
            Context cx = Context.getContext();
        	Object obj = map.get(id);
            return cx.getWrapFactory().wrap(cx, this, obj, cls);
        }
        return result;
    }

    @Override
    public Object get(int index, Scriptable start) {
        if (map.containsKey(index)) {
            Context cx = Context.getContext();
            Object obj = map.get(index);
            return cx.getWrapFactory().wrap(cx, this, obj, cls);
        }
        return Undefined.instance;
    }

    @Override
    public void put(String id, Scriptable start, Object value) {
    	// also make sure that nobody overrides list's interface
        if (super.get(id, start) != null) {
            throw Context.reportRuntimeError1(
                "msg.property.or.method.not.accessible", id);
        }
       	map.put(id, Context.jsToJava(value, cls));
    }

    @Override
    public void put(int index, Scriptable start, Object value) {
       	map.put(index, Context.jsToJava(value, cls));
    }

    @Override
    public Object getDefaultValue(Class<?> hint) {
        if (hint == null || hint == ScriptRuntime.StringClass)
            return map.toString();
        if (hint == ScriptRuntime.BooleanClass)
            return Boolean.TRUE;
        if (hint == ScriptRuntime.NumberClass)
            return ScriptRuntime.NaNobj;
        return this;
    }

    @Override
    public Object[] getIds() {
        Object[] result = new Object[map.size()];
        Iterator<Object> iter = map.keySet().iterator();
        int i = 0;
        while(iter.hasNext()) result[i++] = iter.next();
        return result;
    }

    @Override
    public boolean hasInstance(Scriptable value) {
        if (!(value instanceof Wrapper))
            return false;
        Object instance = ((Wrapper)value).unwrap();
        return cls.isInstance(instance);
    }

    @Override
    public Scriptable getPrototype() {
        if (prototype == null) {
            prototype = ScriptableObject.getClassPrototype(this.getParentScope(),"Object");
        }
        return prototype;
    }

    Map<Object,Object> map;
    Class<?> cls;
}

Tutaj jeszcze słowo wyjaśnienia – żeby komuś nie przyszło do głowy nadpisanie interfejsu Javowego jakimiś głupotami, podczas wstawiania elementów najpierw sprawdzam czy czasem własność o podanej nazwie już nie istnieje i dopiero dopisuję ją do listy. Możnaby się oczywiście pokusić o sprawdzanie, dodawanie gdzieś na boku, udostępnianie przez getIds i odyczt, ale to zostawię już jako ćwiczenie dla czytelnika.

Hi

I found out about rhino some weeks ago and I started looking for some reasonable way to improve my tests by adding support for XmlHttpRequests and I got to this project.

What I found out missing/problematic is support for HTML document parsing, so my idea is to use JTidy but since org.w3c.tidy.Tidy doesn’t support javax.xml.parsers.DocumentBuilder interface I added a small Tidy adapter which extends DocumentBuilder and can be used directly in env.rhino.js.

Here’s short implementation of this idea:

package pl.bedkowski.p.java.tidy;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.io.PrintWriter;

import javax.xml.parsers.DocumentBuilder;

import org.w3c.dom.DOMImplementation;
import org.w3c.dom.Document;
import org.w3c.tidy.Tidy;
import org.xml.sax.EntityResolver;
import org.xml.sax.ErrorHandler;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

public class TidyDocumentBuilder extends DocumentBuilder {

	private Tidy tidy;
	private OutputStream out = new OutputStream() {
		@Override
		public void write(int b) throws IOException {
			// do nothing - this is for getting just the DOMDocument
		}
	};
	private PrintStream err = new PrintStream(out);
	
	public TidyDocumentBuilder(Tidy tidy){
		this.tidy = tidy;
	}
	
	public TidyDocumentBuilder(){
		this(new Tidy());
		tidy.setMakeClean(false);
		tidy.setQuiet(true);
		tidy.setErrout(new PrintWriter(err));
		
	}
	@Override
	public Document newDocument() {
		return Tidy.createEmptyDocument();
	}

	@Override
	public Document parse(InputSource inputSource) throws SAXException, IOException {
		InputStream in = inputSource.getByteStream();
		if( in == null) {
			in = new FileInputStream(inputSource.getSystemId());
		}
		return tidy.parseDOM(in, out);
	}

	@Override
	public void setEntityResolver(EntityResolver resolver) {}
	@Override
	public void setErrorHandler(ErrorHandler errorHandler) {}
	@Override
	public DOMImplementation getDOMImplementation() {return null;}
	@Override
	public boolean isNamespaceAware() {return false;}
	@Override
	public boolean isValidating() {return false;}


}

As you can see there are 2 things to improve:
– it lies in my project’s namespace, which might be removed if Tidy extended DocumentBuilder
– there are still some „blanks” left but it’s already usable in env.js

And than just replace calls in env_rhino.js (line #343)

var htmlParser;
if( !useTidy ) {
    var htmlDocBuilder = Packages.javax.xml.parsers.DocumentBuilderFactory.newInstance();
    htmlDocBuilder.setNamespaceAware(false);
    htmlDocBuilder.setValidating(false);
    htmlParser = htmlDocBuilder.newDocumentBuilder();
}
else {
    htmlParser = new Packages.pl.bedkowski.p.java.tidy.TidyDocumentBuilder();
}
    $env.parseHTML = function(htmlstring){
        return htmlParser.parse(
                  new java.io.ByteArrayInputStream(
                        (new java.lang.String(htmlstring)).getBytes("UTF8")))+"";
    };
    
    var xmlDocBuilder = Packages.javax.xml.parsers.DocumentBuilderFactory.newInstance();
    xmlDocBuilder.setNamespaceAware(true);
    xmlDocBuilder.setValidating(true);
    
    $env.parseXML = function(xmlstring){
        return xmlDocBuilder.newDocumentBuilder().parse(
                  new java.io.ByteArrayInputStream(
                        (new java.lang.String(xmlstring)).getBytes("UTF8")))+"";
    };

Ostatnio przegladajac informacje nt. przyszlosci i kierunkow rozwoju projektu eclipse natknalem sie na informację, że planowane są spore zmiany w systemie pluginów, z których dla mnie najważniejsze na chwilę obecną były:
– systemu okienek w oparciu o XWT (XUL/SWT)
– interfejsu pluginow dostepnego z poziomu języka JavaScript (JS)

Z oboma technologiami miałem już do czynienia, chociażby podczas budowy mojego pluginu do popularnej przeglądarki Firefox. Jednak od razu pojawiło się pytanie – w jaki sposób programiści eclipse’a będą udostępniać interfejsy w JS’ie? Odpowiedź na to pytanie jest już znana od lat – istnieje bowiem biblioteka, będą interpreterem i kompilatorem języka JavaScript w całości napisana w Javie, nazywa się rhino.

Dokumentacja projektu jest dość dobra i pozwoliła mi w ciągu paru godzin ogarnąć możliwości tego rozwiązania, no i narodził się pomysł, żeby w jednym z projektów udostępnić systemowi pluginów także api JavaScript’owe, co mogłoby znacząco wpłynąc na szybkość ich powstawania, a także wykorzystać możliwości np. operatorów czy warunków.

Obecny system pluginów opiera się o 2 interfejsy w Javie i abstrakcyjną klasę bazową, która udostępnia 2 metody, więc dobrze by było, żeby z poziomu JavaScriptu także był do nich dostęp. Nie jest to takie trudne, trzeba tylko, dodać klasie bazowej funkcjonalność obiektu Scriptable, co pozwoli go wykorzystać jako kontekst danego skryptu i umożliwić wywołanie wspomnianych metod z poziomu skryptu.

Jeżeli metody są statyczne to sprawa jest banalnie prosta, bo wtedy można je dodać jako funkcje i wogóle nie trzeba się wysilać:)

Kod poniżej odpowiada za przykładową implementację.

Interfejs:

public interface MyPlugin{
public void executeMyPlugin();
}

Klasa bazowa:

public abstract class MyBaseClass extends ScriptableObject{
public void doSomething(){
}
public void doSomething2(){
}
public static void executePlugins(List<String> pluginsList){
// stwórz kontekst
Context cx = Context.enter();
MyBaseClass executePlugins = new MyBaseClass();
ScriptableObject scope = (ScriptableObject) cx.initStandardObjects(executePlugins);
// dla każdego pliku
// wczytaj plik z dysku i zapisz go w String'u code
String code = "";
// dodaj własności dostępne jako globale z punktu widzenia skryptu
String[] scriptAvailableFunctions = { "doSomething", "doSomething2" };
scope.defineFunctionProperties(scriptAvailableFunctions, MyBaseClass.class, ScriptableObject.DONTENUM);

// dodaj potrzebne importy
String s = "var context = JavaImporter();\n" +
"context.importClass(Packages.MyPlugin);\n" + 
"with (context) {\n " + code + "; p = new MyPlugin({executeMyPlugin:executeMyPlugin});p.executeMyPlugin();\n\n}" + "";

// wykonaj skrypt
cx.evaluateString(scope, scode, "MyScript", 0, null);
}
}

Plugin1.js

function executeMyPlugin(){
  out.println("Hello from plugin1!")
}

Jak widać mój kontekst dołącza do pluginu potrzebne importy, czyli wszystko co jest dostępne w pakietach aplikacji, tak żeby żaden z pluginów nie musiał tego robić sam i wszystko wygląda dobrze, do czasu gdy któryś z nich potrzebuje wykonać jakąś metodę np. z pakietu java.io.

Wtedy można:
– zezwolić mu na samodzielne wykonywanie importu
– stworzyć dodatkowy opis np. w XML’u, który poza ciałem pluginu, mógły zawierać także dodatkowe informacje m.in. o zależnośćiach

Przykładowy XML, mogłby wyglądać tak:

<plugin name="Plugin1" id="Plugin1">
  <dependencies>
    <packages>
      <list>
	java.io;
	java.lang;
      </list>
    </packages>
    <classes>
      <list>
	java.io.File
      </list>
    </clases>
  </dependencies>
  <interfacemethod>
    <&#91;&#91;CDATA
      function executeMyPlugin(){
	out.println("Hello from plugin1!");
      }
    &#93;&#93;>
  </interfacemethod>
</plugin>

I już jest znacznie ładniej, bo poza samym kodem mamy jeszcze trochę metadanych potrzebnych do wykonania pluginu.

Wyobraźmy sobie teraz sytuację, że chcemy w ten sam sposób dodać pluginy korzystające z różnych interfejsów, wtedy jedyne co wystarczy dodać atrybut interface, próbujemy:

<plugin name="Plugin1" id="Plugin1" interface="MyPlugin">
<!-- reszta tak jak byla -->
</plugin>

Co to daje – deklaratywną kontrolę tego co faktycznie zostaje wykonane, do tego możnaby się pokusić o sprawdzenie jakie metody są w interfejsie i sprawdzenie czy aby napewno kod JavaScript’owy zawiera ich deklarację, co w połączeniu z założeniem, że w przyszłości pojawi się do tego jakiś eleganckie gui (np. korzystająć z edytora eclipse’owego w połączeniu z czarodziejem), może dać naprawdę świetne rezultaty.

Pluginy tak naprawdę są elementem pochodnym, mogą być wykorzystane, do wykonywania akcji przewidzianych dla jakichś danych z zwenątrz. Zazwyczaj to jaki plugin ma być wykonany też trzeba w jakiś sposób zadeklarować. Posłużymy się tutaj znowu XML’em:

<map>
<element from="element1" to="element2" plugin="Plugin1" />
</map>

Tutaj mamy prostą postać pluginu, masz element i na nim wykonaj dana operację. Jednak co by było gdybyśmy chcieli stworzyć nowy element na podstawie dwóch innych elementów, trzebaby podać pluginowi skąd ma pobrać wartości, np w ten sposób:

<map>
  <element to="element2">
    <from plugin="Plugin1" separator=";">
      <el>key1</el>
      <el>key2</el>
    </from>
  </element>
</map>

Teraz widać, że plugin będzie miał dostęp do dodatkowych elementów opisywanych przez klucze key1/key2, a wynikiem jego działania ma być połączony string. Idąc dalej tym tropem może się okazać, że przyjdzie potrzeba wykonania na danych operacji matematycznych, np. dodania wartości 2 pól.

<map>
  <element to="element2">
    <from plugin="Plugin1" operation="a+b">
      <el name="a">key1</el>
      <el name="b">key2</el>
    </from>
  </element>
</map>

Jedyna różnica w stosunku do poprzedniego przykładu polega na tym, że wartości pól muszą być zsumowane, aby to zrealizować przy użyciu JS’a, możemy zrobić tak:


var obj = {};
for(el in elements) {
  obj[el.getName()] = inputMap.get(el.value())
}

with(obj){
eval('res='+el.getParentNode().getOperation())
}

out.println("Result of operation is: "+res)

I prosze, dynamiczna typizacja załatwi resztę! Jedyne o co się trzeba zatroszczyć to przechwycenie ew. błędów.