Posts tagged ‘javascript’

For one of my projects based on Zend Framework I do a lot of forms creation and validation but I found out is that there is no way to perform all validation on the client side, which is a huge time-saver.

So I figured that since all form generaing code is already in place and all elements are rendered so nicely – it should be possible to generate validation code with as simple step as just adding one line of decorator. Such a decorator would have to do the following steps:

  1. For each element defined in the form
  2. get defined validators
  3. for each validator generate validation javascript code
  4. get all error messages TRANSLATED if necessarry and apply them when validating (eg. using alert window)
  5. add onsubmit handler for running validation code

One major requirement is to have ONLY standard JavaScript code NO EXTERNAL libraries. Why? Because it just makes things easier. This doesn’t mean that it’s not possible – anyone interested can just extend JsValidation class and perform his own validation.

The idea isn’t new, there already exists an approach that performs javascript validation but it lacks translated error messages. Additionally it contains very bad line:

//Replace Form decorator with our own
$form->removeDecorator('Form');
$form->addDecorator(new CU_Form_Decorator_Form());

Why is it bad, you might ask – along JS decorator one is forced to use SPECIAL form decorator.

Currently my library supports 5 9 – in my opinion* – most widely used decorators :

Updated link to docs.

There’s probably one thing that nees some more explanation – namely the strategy for keeping consistent order of running validation rules. So you do want to have check for non-empty before validating that value is float. In order to achieve it every validator is connected with corresponding STRENGTH integer value which defines the order of execution. So non-empty check is before float but less-than and greater-than might be executed in any order.

In the code it’s realized inside getValidator method whose second returned value is the execution order. Afterwards which value is used by „inner” class ElementValidators inside of its add method which uses it as an array key.

So this is it – the rest should be pretty straighforward – in case it isn’t feel free to leave some comments/questions below. One more remark – currently validation errors are displayed using alert function but yes I’m planning to display them inline the same way ZF’s validation erros are displayed. Erros are displayed the same way regular form validation happens so from user perspective there should be difference – additionally you can provide/override your own Zend.Form.ErrorReporter. Additionally validator rules are defines as functions in Zend.Validator.Rules namespace – each rules has the same name as its PHP counter-part, eg.

Zend.Form.Validator.Rules.Zend_Validate_StringLength = function(value, opts, msgs) {
}

Where:

  • value is the value to be validated
  • opts are options passed to the validator eg. minLength
  • msgs are internationalized messages displayed when validation fails

Source code available on github.

You can also checkout gists for this project

Usage:

<?php  
error_reporting(E_ALL|E_STRICT);  
set_include_path(get_include_path() . PATH_SEPARATOR .  
                 './library');  
require_once 'Zend/Loader.php';  
  
Zend_Loader::registerAutoload();  
//May need to have this set, the JavaScript file paths use baseUrl at the moment  
//Zend_Controller_Front::getInstance()->setBaseUrl('public');  
  

// display validation messages in French
$translator = new Zend_Translate(
    array(
        'adapter' => 'array',
        'content' => '/resources/languages',
        'locale'  => 'fr',
        'scan' => Zend_Translate::LOCALE_DIRECTORY
    )
);
Zend_Validate_Abstract::setDefaultTranslator($translator);

$form = new Zend_Form();  
$form->setView(new Zend_View());  
$form->addDecorator(new Zend_Form_Decorator_JsValidation());  
  
$name = $form->createElement('text', 'name', array(  
        'label' => 'Name'  
));  
  
$name->addValidator('NotEmpty')  
     ->setRequired(true);  
  
$submit = $form->createElement('submit', 'ok', array(  
        'ignore' => true,  
        'label' => 'OK'  
));  
  
$form->setElements(array(  
        $name,  
        $submit  
));  

echo $form->getView()->headScript();  
echo $form->getView()->inlineScript();  
  
echo $form->render();  
  

custom validator (php part):
custom validator (javascript part):

* After one project with ZF

For my javascript project I was looking for an automated tool that would merge all my files into one, which then could be optimized by YUI compressor. I found combiner project developed by nzakas which used require-in-comment approach which was close to what I needed – my JS library uses require function for importing dependencies.

After forkeing it and started code analysis as well as other forks to see what changes would I need to make to adjust it to my needs and it looked like my improvement plan had the same points as most of the forks although I was planning to keep it as simple as possible, yet expandable.

So the plan was:

  • replace jargs with args4j
  • add generics to all collections
  • improve sorting and duplicates handling on the output collection by using TreeSet instead of List and proper Comparator
  • improve cyclic dependency management
  • improve files reading into SourceFile – remove todo List that was used for data exchange between processFile/processFiles methods
  • move file handling functionality into a subclass and thus enable fast exchange of the dependency algorithm – use it for css @import statement

First two points were no-brainers, so they went smoothly.

Sorting/duplicates removal

My biggest problem was the sorting algorithm (3rd point) – in the original version there was List with „manual” checks using List.contains function and after list has been filled with data it was sorted by using Collections.sort and appropriate Comparator.

This all worked fine but I was sure that the List could be replaced with TreeSet where both order and duplicates would be handled at once but original Comparator was a problem. It was good for sorting but not for insertion – when it found two non-related files with equal dependency size it returned 0 which in TreeSet terms meant that file would not be inserted  AT ALL. I checked TreeSet.add method and I found out that it uses binary search algorithm – so there are 2 cases to handle:

  • either there is no more elements, which meant element was inserted
  • Comparator returned 0, which meant that element was replaced.

And this was it – I just had to make sure that elements with the same number of dependencies are handled consistently until there is no more elements or the same element is found. This meant comparing 2 non-related element with the same number of depedencies by name.

Get rid of todo property

Second thing was processSourceFiles method that used extra list in order to exchange dependencies found by processSourceFile method. This looked weird from the beginning, so I was wondering if adding a return value to processSourceFile would help. Afterwards I noticed that processSourceFiles can be called recursively and thus letting go todo list.

Simplify cyclic dependency management

Than there was cyclic dependency handling – there used to be an extra loop that would check all files one-by-one if any of them is not already dependent on currently processed one. So I figured that it would be much easier to do the check inside addDependency which meant getting rid of all overloaded methods and leaving just one that accepts SourceFile object. Finally addDependency method checks if current object is already a dependency of added object (passed as parameter) and if it is, it returns false.

Subclass for reading files

So than removing file reading algorithm and adding Css handler was really easy because dependency reading was already fleshed out, so baiscally I just had to move it outside FileComparator class.

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.

Jak to zwykle bywa pare zbiegów okoliczności:

  • zaczęło się do tego wpisu
  • później dodatkowe pomysły na animacje pojawiały się w nieregularnych odstępach
  • później powstał ten layout
  • później 3 wieczorki i strona gotowa

Z tych właśnie zbiegów okoliczności powstała mała stronka: slidemenus.bedkowski.pl

Jak widać  to oczywiście wycinek możliwości samej biblioteki, ale jakoś trzeba przyciągnąć uwagę.

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.

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.

Na jednym forum pojawiło się pytanie o to jak zrobić przejście między obrazkami no i od razu pomyślałem o Alladynie, a przy okazji chciałem sprawdzić na ile tak naprawdę ułatwia on pracę i nie zawiodłem się – zrobienie animacji zajeło niecałe pół godzinki i poza samym efektem fade, dodałem jeszcze rozwijanie/zwijanie jako dodatkowy bajerek. Gdyby api było bardziej przyjazne niż 3 literowe skróty poszłoby jeszcze szybciej. No cóż trzeba się bedzie tym zająć 🙂

Efekt tutaj

Wersja druga

Kolejna zabawka w java script’cie, tym razem menu, ktore „lata” sobie w poziomie, albo w pionie – dostepne pod tym linkiem, tutaj źródła