Archive for the ‘javascript’ Category

Introduction

In this post I’m going to describe  how cyclic dependencies can cause headache after long hours of debugging and how to solve them using publisher-subscriber design pattern in form of very simple message relay/bus.

I’ll stick to simple environment:

  • jquery 1.10
  • requirejs 2.1
  • jasminejs 1.3.1

There will be some optional dependencies introduced on the way:

  • phantomjs
  • nodejs
  • grunt
  • madgen

After presenting solution I’m going to present a working example with interchangable parts on both notifier and listener side.

Problem description

So there you have it – you built your application using requirejs where modules are decomposed using MVC pattern – model is managed in the DAO layer which communicates with online (remote) and offline (local) database. Configuration for both of them is stored locally in Setting module and at some point Settings got saved in the database which introduced cyclic dependency:

DAO <-> Settings

So the only way to deal with it in current architecture was to give DAO knowledge about Settings and Settings about DAO. This leads to a very complex application structure, which may also be called everyone-knows-everyone.

e.g.


define("settings", ["fs", "dao"], function(fs, dao) {

  var fsSettings = fs.read("settings.json");
  // eval it
  var settings = Object.create(fsSettings, dao.read("settings"));

  function read(key) {
   return settings[key];
  }

  return {
    read: read
  };
});

define("dao", ["settings"], function(settings) {

  function Dao(url){}

  Dao.prototype.init = function daoInit(url) {};

  var dao = new Dao(settings.get('url'));

});

RequireJS solution

You could use special exports keyword (described here) but this means that your modules preserve tight coupling anti-pattern.

Publisher/subscriber – message bus

Publisher subscriber is a design pattern that allows low coupling and high cohesion between different modules of the system. A simple requirements written in BDD/jasmine style would look like this (included below):

describe("message bus", function() {

  it("should allow adding listener for event", function() {

    mBus.addEventListener("myEvent", function(){});

    expect(mBus.length("myEvent")).toEqual(1);

  });

  it("should allow removing listener for event", function() {

    var fn = function(){};

    mBus.addEventListener("myEvent", fn);

    expect(mBus.length("myEvent")).toEqual(1);

    mBus.removeEventListener(fn);

    expect(mBus.listeners("myEvent").length).toEqual(0);

  });

  it("should allow broadcast", function() {

    var fn = jasmine.createSpy('fn'),

    o =  {prop:11};

    mBus.addEventListener("myEvent", fn);

    expect(mBus.length("myEvent")).toEqual(1);

    mBus.notify("myEvent", o);

    expect(fn.calls.length).toEqual(1);

  });

  it("should allow additional data within broadcast", function() {

    var fn = jasmine.createSpy('fn'),

    o =  {prop:11};

    mBus.addEventListener("myEvent", fn);

    expect(mBus.length("myEvent")).toEqual(1);

    mBus.notify("myEvent", o);

    expect(fn.calls.length).toEqual(1);

    expect(fn).toHaveBeenCalledWith(o);

  });
})

As you can see in it is assume that apart from holding references to listeners message bus is stateless, which will makes some things down the line easier.

Let’s make these tests pass!


define(function() {

  var listeners = {};

  function addEventListener(event, fn){
    listeners[event] = listeners[event] || [];
    listeners[event].push(fn);
  }

  function removeEventListener(event, fn) {
    if (listeners[event] === undefined) {
      // do nothing
    }
    else if (listeners[event].length === 1) {
      listeners[event] = []
    } else {
      var id = listeners[event].indexOf(fn);
      if (id !== -1) {
        listeners[event].splice(id, 1);
      }
    }
  }

  function removeAllListeners(event) {
    if (!event) return;
      listeners[event] = [];
  }

  function notify(event, data) {
    if (listeners[event] === undefined) {
      // skip
    } else if (listeners[event].length) {
      listeners[event].forEach(function(fn) {
        fn(data);
      }
    }
  }

  function length(event) {
    return listeners[event] && listeners[event].length || 0;
  }

  function clear() {
    listeners = {};
  }

  return {
    addEventListener: addEventListener,
    removeEventListener: removeEventListener,
    removeAllListeners: removeAllListeners,
    notify: notify,
    clear: clear,
    length: length
  }

});

Rewrite components

So now we have very basic implementation of MessageBus, which can be applied to solve our problems with cyclic dependencies. We’re ready to rewrite our components, in a way that makes makes them independent of each other.


define(["MessageBus"], function(mBus) {

  mBus.addEventListener("dao:init", dbSettingsReady);

  var settings = readFsSettings();
  settingsReady(settings);

  function readFsSettings(){
	return {};
  }

  function settingReady(key, value) {
    mBus.notify("settingReady:" + key, value);
  }

  function dbSettingsReady(dbSettings) {
   settingsReady(dbSettings);

   settings.__proto__ = dbSettings;
  }

  function settingsReady(s) {
    for(var i in s) {
      settingReady(i, s[i]);
     }
  }

// no direct access to settings!

});
describe(["MessageBus"], function(mBus) {
 var dao = new Dao();
 mBus.addEventListener("settingReady:url", function(url) {
    dao.init(url);
    mBus.notify("dao:init");
 });

 return dao;
});

Testing

As you can see some of the logic is hidden behind events and there’s no way to access them directly, so how this makes our test cases easier?

spyies to the rescue!

Using jasminejs spy method we can pretend there’s an instance of mbus and inovke our logic as if there are real interactions.


describe("Settings", function() {

  // prepare mock dependencies
  var mBusMock = jasmine.createSpyObj("mBus", ["addEventListener"]);

  define("mBusMock", function() {
    return mBusMock;
  });

  var r = require.config({
    map: {
      'Settings' : {
        'MessageBus': 'mBusMock',
        'Dao': 'daoMock'
      }
    }
  });

  define(["Settings"], function(s) {
    describe("Settings", function() {
      it("should register 1 event listeners", function() {
        expect(mBusMock.addEventListener.calls.length).toEqual(1);
      });
      it("should register for dao:init event", function() {
        expect(mBusMock.addEventListener).toHaveBeenCalledWith("dao:init", jasmine.any(Function));
      });
      it("should read dao settings", function() {
        mBusMock.addEventListener.calls[0].args[1].call();
      });
    });
  });
});

Pros

No cyclic dependencies

As you can see now modules have no cyclic dependencies so we solved our main issue.

Encapsulation

If you look closer at settings modules you’ll notice, that currently there’s no way to acces its state from outside world, which helps us hide information from outside world.

Testing made easier

As you can see in last requirement for settings – by using jasmnejs spies, we can access registered listener and call it on our own, which helps allows us to test encapsulated logic and does not for module author to break its contract only for the sake of proper testing.

Cons

Application flow

When first entering world of indirect events you might be intimidated and loose control over what happens where – fortunately this is easy to tackle and after relatively small amount of time, when your way of thining adjusts to having „man-in-the-middle” you’ll discover a completely new world ahead of you – it really is worth to try!

Soft dependencies

As mentioned earlier there are no direct dependncies between modules, so static code analysis is much harder if possible at all but in case of such a dynamic language as JavaScript this should not be an issue – just prepare yourselves for it by introducing proper conventions and follow it consistently.

Exception handling

Since you have no direct access to your callee, you can find it difficult to eliminate bugs from your software. Just prepare yourself for situations where your subscribers throw an error and handle it consistently, e.g. by logging it.

You might also try to post another error event but beware not to create a deadlock.

Event driven lifecycle

This is somewhat similar to point #1 – in the beginning you can find it hard to properly define your modules/application lifecycle, because each of them will receive different event. After a while you’ll find out that grouping events and hiding them behind message bus makes your life much, much easier.

Summary

During this article I’ve gone through possible headaches caused by cyclic-dependencies with requirejs and pains it might cause.

Next thing was to decompose modules so they’re totally clueless of one another and communicate only through message bus.

Than I showed how to tackle testing with jasminejs spies and how testing was made much easier through real encapsulation of logic inside a module.

Useful links

 

Dodałem do repozytorium druga wersje skryptu sticky buttons, poprawki zawierają:

  • poprawione zachowanie w przypadku przewijania kontenera
  • dodanie rekurencyjnego wyliczania animacji po zjechaniu z guzika
  • animacja po zjechaniu jest tylko „odtwarzana”, klatka po klatce, co powinno przyspieszyć jej przetwarzanie
  • poprawione zachowanie w przypadku złapania guzika
  • poprawione zachowanie w przypadku przejechania po liście guzików (problem z płynnością animacji)

Do tego cała masa zmian w kodzie – można powiedzieć, że skrypt jest napisany od zera. Zawiera ujednolicony system obsługi układu współrzędnych poprzez klasę Point, która zwiera metody manipulujące współrzędnymi x,y odpowiednio. Ona także unifikuje jquery’owe left/right oraz pageX/pageY z eventa.

Dopisałem także funkcyjkę bind służącą wiązaniu parametrów funkcji do późniejszego wykorzystania. W przypadku zakończenia animacji, wysyłane jest zdarzenie, którego obsługa powoduje wygaszenie wątku animacyjnego jak również wykrycia zmiany położenia elementu w przypadku np. scroll’owania zewnętrznego kontenera i zakutalizowania swojej pozycji.

Ten wpis będzie dotyczył zwiekszania czytelności kodu podczas dynamicznego nadpisywana metody w JavaScript’cie.

Problem jest banalny – mamy sobie jakas klase, która oferuje 2 metody, getName/setName i chcielibyśmy w zupełnie innym miejscu dla jednej instancji zamienić implementację setName (poprzednia nas nie interesuje).


function MyClass(){}

MyClass.prototype.getName(){}

MyClass.prototype.setName(){}

Jak widać na powyższym kawałku kodu, przykład jest banalny, teraz utwórzmy sobie instancję MyClass:


// pierwsza instancja
var m1 = new MyClass();

// druga z nadpisana metoda
var m2 = new MyClass();

m2.setName = function newSetName() {}

Na pozór wszystko wydaje się cacy – tworzymy sobie 2 instancje, z których jedna ma troche zmienione zachowanie, wszystko przecież jest czytelne. Owszem, ale co sie stanie jeżeli tworzenie instancji m1 i m2 będzie w znacznie oddalone od miejsca ich uzycia? Wtedy deklaracja MyClass nie da nam żadnej wskazówki, że jej zachowanie może sie zmienić. Jak temu zaradzić – można przesłać metode w konstruktorze, oto pomysł:


//dodajmy parametr i jakies jsdocki

/**
 *@param Function [setName=undefined]
 */
function MyClass(setName) {
   if (setName) {
      this.setName = setName;
   }
}

// reszta bez zmian

Jak widać mamy domyślną implementacje w prototypie a dodatkowo podczas tworzenia klasy, przesyłamy jej funkcję zastepująca setName, wtedy utowrzenie m1 i m2 wyglądałoby tak:


var m1 = new MyClass();

var m2 = new MyClass(function newSetName(){});

Jak widać dodanie parametru do konstruktora, któremu towarzyszy dopisanie krótkiego jsDoc’a zajmuje 5 minut a w zupełności wystarczya za dokumentację.

Latest version numbered 2.79 of Hermes Java File Uploader is out.

This release is caused by problems with Internet Explorer and java6. You can download your free demo at product page

Pewnie niektórzy nie znają, ale pare lat temu pojawiło się bardzo miłe dla oka menu na stronie mjau-mjau, nazwane „sticky buttons”, jednak wykonane w technologii Flash. Ja od jakiegoś czasu rozgryzam jQuery i uznałem, że ciekawym sposobem na podciągnięcie swoich umiejętności, będzie przepisanie takowego menu.

Efekty można zobaczyć na stronie Dominiki Osadców, natomiast źródełko tutaj.

Miłej zabawy.

Trochę upłynęło czasu od ostatniego wpisu, ale byłem dość zajęty pracą na dwoma niezmiernie ciekawymi projektami. Oba są związane ze skryptami ECMA, jeden z nich dotyczy języka JavaScript i jest rozszerzeniem projektu Alladyn w wersji 1.6. Pomysł chodził mi po głowie od dawna, ale dopiero do niedawna dojrzał na tyle, iż uznałem, że czas sie za niego zabrać.

Podstawowe założenia:

  • dodawanie klatek kluczowych poprzez metode addFrames
  • dodawanie klatek oznaczonych labelką
  • wywoływanie animacji „ciągiem” korzystając z metody play
  • wywoływanie animacji korzystając z numeru klatki
  • wywoływanie animacji korzystając z labelki
  • możliwość definowania sztucznej własności, która zawiera callback do skryptu, który ma być wykonany w danej ramce
  • integracja z jQuery
  • definiowanie własności clip korzystając z cztero-elementowej tablicy (clip:rect)
  • dodanie obsługi własności dotyczących kolorów (background-color/border-color/color)
  • obsługa kolorów poprzez trój-elementową tablicę kolejnych składowych RGB
  • dodanie obslugi rownan ruchu Robert’a Penner’a

Wszystkie elementy udało się zrealizować, wynik można obejrzeć tutaj.

Dorzuciłem także parę przykładów:

Zapraszam również do mojego nowszego wpisu, slidemenus oraz strony z demo.

Jeżeli podoba Ci się ten projekt to zapraszam również do obejrzenia, projektu obiektowych komententów skyjs.

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.

Ostanio miałem okazję zmierzyć sie z rozszerzeniem do popularnej przeglądarki Firefox, które miało umożliwiać wykonywanie testów Unitowych w JavaScript’cie. Jak zwykle nad zaletami testów ani wprowadzenia do xula robić nie będę, bo po co 🙂 Chciałem wspomnieć o problemie z jakim przyszło mi się zmierzyć.

Otóż plugin ma umożliwiać uruchamianie testów napisanych w JavaScript’cie, a jak wiadomo jest on obsługiwany przez przeglądarke, więc trzeba było znaleźć sposób, żeby można było nieinwazyjnie z dokumentu html pobrać zadeklarowane testy, uruchmić je jeden po drugim dostarczając jednocześnie elegancki pasek postępu użytkownikowi. Niby proste, bo w końcu pasek postępu mam gdzieś już zrobiony, w samym JS’ie też jakiś początkujący nie jestem, przygoda z GTK2+/GTKmm, pogłębiła wiedzę nt budowania interfejsu i ogólnie w ciemię bity nie jestem, więc powinno pójść gładko.

Troche szukania i znalazłem to czego mi trzeba, XUL udostępnia 2 bardzo przyjemne obiekty: iframe i browser, ale do pełni szczęścia, trzeba dać możliwość aktualizowania już załadowanych testów przed wykonaniem. No niby proste, na obiekcie webNavigation, wywołujemy metodę reload, przypisujemy zdarzenie onload do obiektu contentDocument, jak opisuje to dokumentacja i nie działa 😀

W sumie to dałbym sobie spokój, ale wrzuciłem mój kawałek kodu na stronę z rozszerzeniami FF, no i zgłosił się drugi taki jak ja, któremu tego brakuje, więc trzeba było przysiąć trochę i poszukać, jak to obejść.

Już już miałem się poddać, gdy znalazłem dyskusję na forach mozilli opisującym ten sam problem, wraz z rozwiązaniem, a jest ono banalne. Zamiast czytać dokumentację, trzeba po staremu dopisać sobie zdarzenie do firame’a i tak czekać spokojnie aż do nas przwędruje.

Poprawka gotowa, użytkownik/klient zadowolony, więc można znowu zasnąc snem sprawiedliwego:)