Posts tagged ‘java’

Ostatnio zacząłem zmagać się z problemem samodzielnego podpisywania appletów javowych. Jest coprawda para narzędzi keytool/jarsigner, jednak mi zależy na rozwiązaniu pozwalającemu podpisać applet z aplikacji javowej przy pomocy wygenerowanego wcześniej klucza.

Oto poszczególne kroki:

  • wygeneruj certyfikat przy pomocy narzędzia keytool i dodaj go do aplikacji
  • dla każdego pliku z docelowego archiwum wygeneruj sume kontrolną (sha-1) i zakoduj bajty metodą base64
  • połącz sumy kontrolne i wygeneruj połączoną sumę kontrolną
  • do pliku *.SF dodal sumy kontrolne
  • dodaj wcześniej wygenerowany certyfikat

Po dotarciu do kroku drugiego pojawił się mały problem – otóż java nie dostarcza domyślnej implementacji algorytmu base64, ale przecież jest taki popularny… Jest takowy algorytm dostarczany przez suna w pakiecie sun.misc, ale ponoć może on zostać usunięty bez ostrzeżenia.

Troche guglania i znalazłem gotowe rozwiązanie, udostępnione przez autora dla wszystkich na zasadzie dobra wspólnego. No i super sprawa rozwiązana, niemniej jednak znalazłem też szybki kurs dla chętnych. Opis samodzielnej implementacji algorytmu , więc jako, że miałem to w planach od jakiegos czasu to zabrałem sie do roboty.

Jest też zadanie domowe – tutorial opisuje jak zrobić kodowanie natomiast odkodowywanie trzeba zrobić samemu.

Już jakiś czas minął od ostatniego wpisu, ale zajęty byłem m.in. zdawaniem egzaminów na stronie javaBlackBelt.com. Sama strona jest bardzo ciekawa – pytania dodaja użytkownicy, są moderatorzy, którzy je oceniają i zatwierdzają. Same testy są dość ciekawe, dla kogoś obeznanego z Javą nie powinny stanowić problemu. Egzaminy zdaje się za punkty, które można zdobyć m.in. poprzez dodawanie nowych pytań, ale jeżeli się zdaje pewne egzaminy w odpowiedniej kolejności to są one za free, to wystarcza gdzieś do zielonego pasa, później już trzeba troche punktów natrzaskać. Ja doszedłem do niebieskiego i narazie mi się wena skończyla, no ale pewnie wróci za jakiś czas.

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>
    <[[CDATA
      function executeMyPlugin(){
	out.println("Hello from plugin1!");
      }
    ]]>
  </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.

Batalii z JAXB 2.1 ciąg dalszy :)

Po tym jak udało mi się zmusić XJC do wygenerowania schemy, dodania do wygenerowanego kodu @XmlJavaTypeAdapter i zamiany „w locie” atrybutu na kolekcje (opisane tutaj) pojawił sie mały problem. Po próbie dodania do <xjb:javaType /> silnie typizowanej kolekcji w następujący sposób:

<xjc:javaType name="java.util.Map&lt;String,String&gt;" adapter="com.nsn.tariffmig.parser.output.LliLibReferenceAdapter" />

<!-- tak tak pamietajcie, że to xml, więc > i < trzeba zapisywać jako encje -->


Pojawił się następujący problem w wygenerowanym kodzie:


import java.util.Map<String,String>;

Czyli nie całkiem to o co chodziło :( Na szczęście źródła do JAXB są dostępne, więc postanowiłem sprawdzić ile zajmie znalezienie kodu, odpowiedzialnego za wypluwanie „import” cośtam. Okazało się to bajecznie proste, szybki grep:

grep -nirs 'import' *.java | less

Pojawiło się dość sporo wyników ale przecież import to słowo kluczowe w Javie. Z drugiej jednak strony nietrudno było odróżnić to czego szukałem, bo większość wpisów to prawdziwe importy, np:

com/sun/codemodel/JPackage.java:50:import java.util.Iterator;
com/sun/codemodel/JPackage.java:51:import java.util.List;
com/sun/codemodel/JPackage.java:52:import java.util.Map;

Natomiast ja szukałem tego:

com/sun/codemodel/JFormatter.java:110: importedClasses = new HashSet<JClass>();
com/sun/codemodel/JFormatter.java:266: * decide what types to import and what not to.
com/sun/codemodel/JFormatter.java:273: if(importedClasses.contains(type)) {

Teraz wystarczy tylko dodać „wymazywanie typu :D ”, czyli zamienić linię 438 z:

p(„import”).p(clazz.fullName()).p(‘;’).nl();

na:

p(„import”).p(clazz.fullName().replaceAll(„<.*?>$”, „”)).p(‘;’).nl();

I już – kompilujemy, wkładamy to do jara (nie zapomnijcie o licencjach) i już możemy odpalać taska XJC z adapterami do silnie typizowanych kolekcji.

Prawda, że proste…

Jednym z problemów z którym trzeba się zmierzyć podczas pracy programisty to spadek wydajności, na który standardowy środek zaradczy to prosty cache oparty o HashMap’e, w której kluczami są parametr(-y) przesłany do metody. Myślę, że zasada działania jest dość powszechnie znana, ale tak dla pewności ją powtórzę:

- sprawdź czy dal danego argumentu (argumentów) wpis znajduje się w mapie
- jeżeli nie wykonaj metodę
- jeżeli jest to zwróć wartość znalezioną w mapie

Mapa sprawdza się znakomicie, jednak pozostawia mały niesmak w ustach, bo co jeżeli zechcemy zrezygnować z cache’owania tego typu, albo wymyślimy super algorytm dzięki któremu cache nie będzie już potrzebny?

Te i inne problemy opisuje w swoim artykule „Memoization in Java” Tom White, przedstawiając także elegancka refaktoryzację powyższego algorytmu w celu oddzielenia mechanizmu cache’owania od obiektu docelowego. Jednak jego celem jest technika idąca krok dalej, a oparta  o wywołaniu metody przez dynamicznie zbudowane proxy (dynamic proxy), które wewnętrznie zbuforuje wynik dla danych argumentów i następnym razem już nie trzeba będzie czekać nieskonńczenie długo na wynik. Jest to szczególnie przydatne w sytuacji, gdy identyczne wyniki są zwracane przez metodę operująca na innej instancji obiektu, porozwalanych w różnych miejscach w kodzie, a poprzednie wywowałnie dawno zgineło w czeluściach garbage-collectora.

I jeszcze jedno – artykuł jest datowany na 2003, więc może się rodzić pytanie skąd ta Java5 w tytule? Otóż wraz z tą wersją jezyka, usprawniono mechanizm dynamicznego proxy na tyle, że działa on 1,5 do 2 razy wolniej od zwykłego wywolłania metody w porównaniu z 40-to krotnym opóźnieniem w przypadku starszych wersji. Wyglada na to, że warto się z tym zmierzyć :)

Niby nic, a jednak. Wymyśliłem sobie, że pewien program w Javie zamiast wypluwać XML’a, który później musi być przyjęty jako konfiguracja innego programu w Javie będzie generował bytecode klasy z wypełnionymi danymi, tak, żeby nie trzeba bylo tracić ani czasu ani zasobów na konwersje XML’a do jakiegos znośnego formatu.

Jak zwykle trzeba sie na początek troche porozglądać, żeby sprawdzić jakie są dostępne narzędzia. Do głowy przyszło mi wykorzystywane w springu i hibernate cglib, ale strone mają jakąś toporną, więc szukałem dalej. Moje wcześniejsze poszukiwania opcode’ów w php , zwróciło moją uwagę w kierunku specyfikacji Virtualnej Machiny :) , no i rzuciłem ją na tapetę, ale że to kobyła i do tego nudna jak flaki z olejem, to postanowiłem wypróbować czegoś w praktyce.

Poszukiwania przywiodły mnie do bilioteki asm.

Pozostało tylko pisać, ale asm oferuje jeszcze jedno świetne narzędzie, plugin do eclipse’a pozwalający:

- podejrzeć bytecode danej klasy

- podejrzeć kod samego ASM potrzebny do wygenerowania aktualnie wybranego fragmentu kodu

Szczególnie druga  opcja daje spore pole do popisu. Można po prostu napisać kawałek kodu w edytorze, zaznaczyć go i zobaczyć jakie instrukcje trzeba wykonać, żeby coś identycznego wypluć jako bytecode. Dla mnie świetna sprawa do pokombinowania troche, świetnej zabawy, a przy okazji szybkiego zdobywania wiedzy.

Oto wynik:

import java.io.BufferedOutputStream;
import java.io.DataOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

import org.aspectj.org.objectweb.asm.ClassWriter;
import org.aspectj.org.objectweb.asm.Label;
import org.aspectj.org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Constants;

public class Bla implements Constants {
public Bla(){

}

public void bla(String s){
System.out.println(s);
}
public static void bla(int s){}

public static void main(String[] args) throws Exception {
ClassWriter cw = new ClassWriter(false);
cw.visit(
V1_5, // version
ACC_PUBLIC+ACC_SUPER, // access
"asm1/Notifier", // class name
null, // signature
"java/lang/Object", // super class
(String[])null // interfaces
);

MethodVisitor mv;
mv = cw.visitMethod(ACC_PUBLIC, "", "()V", null, null);
mv.visitCode();
Label l0 = new Label();
mv.visitLabel(l0);
mv.visitVarInsn(ALOAD, 0);
mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "", "()V");
Label l1 = new Label();
mv.visitLabel(l1);
mv.visitInsn(RETURN);
Label l2 = new Label();
mv.visitLabel(l2);
mv.visitLocalVariable("this", "LBla;", null, l0, l2, 0);
mv.visitMaxs(1, 1);
mv.visitEnd();

mv = cw.visitMethod( ACC_PUBLIC,
"notify", // method name
"(Ljava/lang/String;)V", // description
"", // method descriptor
new String[]{} // exceptions
);
mv.visitCode();
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitVarInsn(ALOAD, 1);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V");
mv.visitInsn(RETURN);
mv.visitMaxs(2, 2);
mv.visitEnd();

cw.visitEnd();

DataOutputStream out = new DataOutputStream(new BufferedOutputStream(new FileOutputStream("/tmp/asm1/Notifier.class")));
out.write(cw.toByteArray());
out.flush();
out.close();

Class c = Class.forName("asm1.Notifier");

Object o = c.newInstance();

Method m = c.getMethod("notify", new Class[]{String.class});
m.invoke(o, new Object[]{"Hello World!"});
m.invoke(o, new Object[]{"Some other text :) "});
}

}

Jak widać, kod np konstruktora jest żywcem przepisany z okienka „bytecode”, ale co najważniejsze TO DZIAŁA!

Pare pomocnych wskazówek:

- odpalenie skompilowanego programu spod Eclipse poprzez run, nie zadziałało, pokazał mi błedy z zarządzaniem zabezpieczeniami i tyle sie skończylo, ale po odpowiednim dostarczeniu argumentów z wiersza poleceń (używając -Xbootclasspath/p: orac -classpath) działa jak złoto

- sam „bytecode plugin” nie działa w wersji Eclipse’a 3.4 i trzeba sobie dodać nowy URL do listy źródeł oprogramowania, a na liście pojawią się pluginy dla tejże wersji

- jak zapewne widać moja klasa Bla, posłużyła za szablon instrukcji jakie trzeba wykonać, żeby wypisać poprawny bytecode (metoda bla oraz konstruktor) a piszę o tym po to, żeby zaczęcić do tejże metody