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.