Dynamiczna Java


Dynamiczne programowanie pozwala na tworzenie bardzo elastycznych i uniwersalnych aplikacji. W Javie ma ono wiele aspektów.
Java jest językiem interpretowanym. Kompilator tworzy kod bajtowy, ten zaś podlega interpretacji przez wirtualną maszynę Javy. Daje to podstawy do dynamicznego, w fazie wykonania, kształtowaniu mechanizmów działania programu. Osiągamy to dzięki tzw. refleksji.
Refleksja stanowi również podstawowy, niskopoziomowy mechanizm, wykorzystywany przy programowaniu komponentowym (JavaBeans). Termin JavaBeans wiąże się  z koncepcją budowania programów z gotowych, łatwo dostosowywanych do konkretnych potrzeb, komponentów programowych. Idea to niezwykle kusząca, ale jednocześnie – z niewiadomych powodów – owiana nimbem "wyższego wtajemniczenia".  W bardzo krótkim wprowadzeniu do JavaBeans zobaczymy, że nie ma tu nic tajemniczego ani trudnego, a korzyści wiążą się nie tylko z ponownym uzyciem gotowych komponentów, ale również z możliwością tworzeniu uniwersalnych kodów dzięki machanizmowi nasłuchu zmian właściwości.
Kolejny dynamiczny aspekt Javy - to adnotacje. Pozwalają one na swoistym metapoziomie kształtować semantykę aplikacji i znacząco upraszczają wykorzystanie różnych technologii programistycznych.
Ostatni temat w tym wykładzie dotyczy integracji Javy z  językami skryptowymi. Tu dynamiczne programowanie w Javie przekracza bariery jednej platformy i otwiera przebogate możliwości w tworzeniu różnorodnych aplikacji. 

1. Refleksja

1.1. Dynamiczne ładowanie klas

W klasie Object zdefiniowano metodę getClass. Zastosowana wobec dowolnego obiektu zwraca odnośnik do jego klasy, do obiektu klasy java.lang.Class.

Obiekty klasy Class są klasami (to ważne: tu same klasy są obiektami)


Możemy wobec takich obiektów stosować różne metody klasy Class z pakietu java.lang, np.

Obiektów-klas nie możemy tworzyć za pomocą wyrażenia new.
Jedynie poprzez użycie odpowiednich metod uzyskujemy odnośniki do tych obiektów.

Jedną z takich metod jest statyczna metoda klasy Class:

        forName(String NazwaKlasy);

Np. pisząc:

Class c = Class.forName("javax.swing.JButton");

lub

Class c = javax.swing.JButton.class;  <--- nazwa_klasy.class jest literałem oznaczającym
                                                            obiekt-klasę

uzyskujemy odnośnik do  klasy przycisków i możemy się nim posłużyć przy tworzeniu obiektu:

JButton b = (JButton) c.newInstance();
  
W statycznym przypadku, gdy wszystko jest ustalone "w źródle" programu, sens takich konstrukcji jest niewielki. Ale bardzo często warto odłożyć pewne ustalenia do fazy wykonania programu, zwiększając jego elastyczność i uniwersalność. Wtedy dynamiczna reprezentacja obiektów-klas bardzo się przydaje.

Najprostszy przykład: odroczenie ustalenia sposobu obsługi akcji na przycisku do fazy wykonania programu pokazano na poniższym wydruku.  Jako argument podajemy nazwę klasy, której obiekt obsługuje akcję. Możemy mieć wiele takich (wariantowych) klas i bez żadnej rekompilacji zmieniać sposoby obsługi kliknięcia w przycisk.
package dynload;
import javax.swing.*;

class Main extends JFrame {

  static void exit(String s) {
    System.out.println(s);
    System.exit(1);
  }


  Main(String actionClassName) {
    Class actionClass = null;
    Action act = null;
    try {
      actionClass =  Class.forName(actionClassName);
      act = (Action) actionClass.newInstance();
    } catch (Exception exc) {
        exit("Obiekt klasy akcji nie może być utworzony");
    }

    JButton b = new JButton();
    b.setAction(act);
    add(b);
    setDefaultCloseOperation(DISPOSE_ON_CLOSE);
    pack();
    setLocationRelativeTo(null);
    setVisible(true);  }
  
  public static void main(String args[]) {
    final String in = JOptionPane.showInputDialog("Podaj nazwę klasy:");
    if (in != null) {
      SwingUtilities.invokeLater(new Runnable() {
        public void run() {
          new Main(in);
        }
      });  
     
    }
  }

}

Uwaga: w powyższym przypadku newInstance zwraca referencję do obiektu klasy Object - stąd potrzeba konwersji do typu Action.

Jeśli teraz przygotujemy dwie klasy, opisujące różne akcje np.
package dynload;
import javax.swing.*;
import java.awt.event.*;

public class DialogAction extends AbstractAction {

  final static String ACTION_NAME = "Show msg";

   public DialogAction() {
     super(ACTION_NAME);
   }

   public void actionPerformed(ActionEvent e) {
     JOptionPane.showMessageDialog(null, ACTION_NAME);
   }

}
i
package dynload;
import javax.swing.*;
import java.awt.event.*;

public class PrintAction extends AbstractAction {

  final static String ACTION_NAME = "Print";

   public PrintAction() {
     super(ACTION_NAME);
   }

   public void actionPerformed(ActionEvent e) {
     System.out.println("Wykonan akcja: " + ACTION_NAME);
   }

}
to uruchomienie głównego programu z argumentem: DialogAction:

java Main dynload.DialogAction

spowoduje, że przycisk w oknie uzyska nazwę Show Msg, a jego klikniecie otworzy okienko komunikaty.

Natomiast po uruchomieniu programu z argumentem PrintAction

java Main dynload.PrintAction

nada przyciskowi nazwę Print, a jego kliknięcie wyprowadzi komunikat na konsolę.

Uwaga: jak widać podawane nazwy klas winny być kwalifikowane nazwą pakietu.

Takie możliwości istniały w Javie od zawsze. Ale samo dynamiczne ładowanie klas to zdecydowanie za mało, by można było tworzyć naprawdę elastyczne programy. Dopiero w wersji 1.1  Java zyskała prawdziwą elastyczność dzięki wprowadzeniu mechanizmów refleksji.

1.2. Mechanizm refleksji


Podstawowy programistyczny interfejs refleksji (Core Reflection API) realizowany jest przez klasy pakietu java.lang.reflect  oraz rozbudowaną klasę Class z pakietu java.lang.

Refleksja oznacza możliwoć wykonywania W TRAKCIE WYKONANIA PROGRAMU następujących działań: 


Użycie refleksji pozwala m.in. na:
 
Uzywając mechanizmów refleksji do metod i pól odwołujemy się poprzez ich nazwy, a nie identyfikatory.
Na czym polega różnica wobec statycznego przypadku?

W statyce odwołania są skonkretyzowane na etapie kompilacji.
Piszemy np. b.getText(). I tak już zostanie na zawsze.
W dynamice konstrukcja jest całkiem inna - właśnie posługująca się nazwą metody. Piszemy raczej tak: b.invokeMethod("getText").
Tu invokeMethod jest naszą własną metodą. Jako argument podajemy nazwę metody klasy, a ponieważ jest to String, możemy go zmieniać w każdym momencie wykonania programu. Np. możemy napisać: b.invokeMethod(s), a kolejne podstawienia pod s różnych nazw metod będzie zmieniać znaczenie tej linii programu. Właśnie w invokeMethod używamy środków refleksji.
 
Klasy pakietu java.lang.reflect
KlasaPrzeznaczenie
Array Tworzenie tablic, uzyskiwanie i ustalanie wartości elementów
Constructor Informacja i dostęp do danego konstru ktora danej klasy. W szczególności wykorzystanie dla tworzenia obiektu.
Field Informacja i dostęp do pola obiektu. Pobranie i zmiana wartości pola.
Method Informacja o danej metodzie danej klasy. Dynamiczne wywołanie metody na rzecz danego obiektu.
Modifier Uzyskiwanie informacji o modyfikatorach składowej obiektu lub klasy.

 
Użyteczne metody klasy java.lang.Class
 
MetodaPrzeznaczenie
getClasses() 
getDeclaredClasses() 
Zwraca tablicę obiektów klasy Class, które są składowymi danej klasy.
getConstructors() 
getDeclaredConstructors()
Zwraca tablicę obiektów klasy Constructor; są to konstruktory danej klasy
getConstructor(Class[]) getDeclaredConstructor(Class[]) Zwraca obiekt konstruktor (obiekt klasy konstruktor), który ma podane typy argumentów 
getMethods() 
getDeclaredMethods() 
Zwraca tablicę, zawierającą odnośniki do metod klasy. Metody są obiektami klasy Method.
getMethod(String, Class[])
getDeclaredMethod(String, Class[]) 
Zwraca metodę o podanej nazwie i podanych argumentach jako obiekt klsy Method.

Uwaga : Rozróżnienie pomiędzy metodami mającymi i nie mającymi w nazwie tekstu "Declared" jest następujące:

1.3  Przykłady wykorzystania refleksji

Przypomnijmy sobie najpierw prezentację różnych rodzajów ramek (wyklad o komponentach GUI).
Posłużył jej następujący program, w którym w sposób dynamiczny uzyskujemy informacje o polach klasy.
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.border.*;
import java.lang.reflect.*;

public class Ramki extends JFrame {

   // Pola klasy określają różne rodzaje ramek
   private Border
     // empty = BorderFactory.createEmptyBorder(),
     blackLine = BorderFactory.createLineBorder(Color.black),
     blueLine3 = BorderFactory.createLineBorder(Color.blue, 3),
     // titled1 = BorderFactory.createTitledBorder("Tytuł"),
     titled2 = new TitledBorder(blueLine3,"Tytuł",
                   TitledBorder.CENTER, TitledBorder.CENTER,
                   new Font("Dialog", Font.BOLD | Font.ITALIC, 16),
                   Color.blue),
     etched = BorderFactory.createEtchedBorder(),
     etchedC = BorderFactory.createEtchedBorder(Color.red, Color.yellow),
     // raisedBevel = BorderFactory.createRaisedBevelBorder(),
     // loweredBevel = BorderFactory.createLoweredBevelBorder(),
     matteColor = BorderFactory.createMatteBorder(5, 10, 5, 15, Color.red),
     matteIcon = new MatteBorder(new ImageIcon("FastForward24.gif")),
     softBevR = new SoftBevelBorder(SoftBevelBorder.RAISED),
     softBevL = new SoftBevelBorder(SoftBevelBorder.LOWERED),
     compound1 = BorderFactory.createCompoundBorder(softBevR, softBevL),
     compound2 = BorderFactory.createCompoundBorder(blueLine3, compound1),
     compound3 = BorderFactory.createCompoundBorder(compound1, matteIcon);


  Ramki() {
    super("Prezentacja ramek");
    getContentPane().setLayout (new GridLayout(0,4,5,5));

    // Klasa tego obiektu
    Class c = getClass();

    // Uzyskanie tablicy wszystkich zadeklarowanych pól tej klasy
    Field[] field = c.getDeclaredFields();

    // Przebiegamy po polach-ramkach
    for (int i=0; i< field.length; i++) {

      // Nazwa pola (zmiennej) - opisującego kolejną ramkę
      String fldName = field[i].getName();

      // Tę nazwę wypiszemy na etykiecie
      JLabel l = new JLabel(fldName, JLabel.CENTER);

      // Uzyskanie referencji do obiektu, reprezentowanego przez
      // pole field[i] tego (this) obiektu. Czyli - do kolejnej ramki

      Object ramka = null;
      try {
        ramka = field[i].get(this);
      } catch (IllegalAccessException exc) { // Ten wyjatek może wystąpić
          exc.printStackTrace();             // gdy dostęp do pola jest zabroniony
      }                                      // np. z innej klasy do prywatnego pola

      // Dostaliśmy oczywiście ramkę, ale jako Object
      // - konieczna konwersja do Border

      l.setBorder((Border) ramka);

      getContentPane().add(l);
    }

    setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    pack();
    show();
  }

  public static void main(String[] args) {
    new Ramki();
  }
}
Skrupulatny Czytelnik zapewne zauważy, zę prezentacja jest teraz nieco inna niż w rozdziale o GUI (zob. rysunek).

r

W programie, w definicjach pól klasy zmieniono (w stosunku do poprzedniej prezentacji) niektóre własciwości ramek, niektóre zaś ramki usunięto (za pomocą komentarzy). Wszystkie manipulacje na zestawie ramek do prezentacji - dzięki refleksji - wykonujemy wyłącznie "na" polach klasy (definiujących ramki). Możemy więc w prosty sposób dodac nowe ramki, i - jak widzieliśmy - usunąć inne.
Takie zastosowanie refleksji  znacznie ograniczyło pracochłonności pisania kodu.

Drugi przykład dotyczy dynamicznego wywołania metod.
Program daje użytkownikowi wybór co do następstw przyciśnięcia jakiego przycisku. Co więcej, wyborów takich użytkownik może dokonywać w fazie wykonania programu.
Zestaw możliwych akcji (na przyciskach) będzie zawarty w klasie ActionsSet np.
public class ActionSet {

 public void dodaj() { show("Dodaj"); }
 public void usuń() { show("Usuń"); }
 public void zastąp() { show("Zastąp"); }
 public void szukaj() { show("Szukaj"); }
 public void otwórz() { show("Otwórz"); }

 private void show(String string) {
   JOptionPane.showMessageDialog(null, string);
 }

}
Opcje dla użytkownika będą przedstawione w menu kontekstowym, otwieranym na przycisku. Z tego menu może on wybrać (wielokrotnie i różnie w trakcie działania programu) co konkretnie ma się stać, jeśli przyciśnie ten przycisk.

Poniższy wydruk pokazuje konstrukcję programu. Warto zwrócić uwagę na zmienną liczbę argumentow metod refleksji.
import java.lang.reflect.*;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

public class ReflectionTest extends JFrame implements ActionListener {

  Method currAction = null; // bieżąca metoda obsługi
  Class actionClass = null; // klasa obsługi
  Object actionObject = null; // obiekt obsługi
  JPopupMenu popUp = null; // menu kontekstowe z wyborem obsługi
  JButton b;

  public ReflectionTest() {
    super("Test refleksji");
    try {
      actionClass = Class.forName("ActionSet");
      actionObject = actionClass.newInstance();
    } catch (Exception exc) {
      throw new RuntimeException("Wadliwa klasa obsługi");
    }

    popUp = new JPopupMenu();
    createMenuItems();

    b = new JButton("Użyj prawego klawisza myszki, by ustalić akcję");
    b.setFont(new Font("Dialog", Font.BOLD, 24));
    b.addActionListener(this);
    b.setComponentPopupMenu(popUp); 

    setLayout(new FlowLayout());
    add(b);
    setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
    pack();
    setLocationRelativeTo(null);
    setVisible(true);
  }

  void createMenuItems() {
    Method mets[] = null;
    try {
      mets = actionClass.getMethods();
    } catch (Exception exc) {
      throw new RuntimeException("Niedostępna info o metodach klasy obsługi");
    }

    for (Method m : mets) {
      if (m.getDeclaringClass() == actionClass) {
        String name = m.getName();
        JMenuItem mi = new JMenuItem(name);
        mi.addActionListener(this);
        popUp.add(mi);
      }
    }
  }

  void setCurrentAction(String action) {
    try {
      currAction = actionClass.getMethod(action); // zm. liczba arg.!!!
      b.setText("Teraz akcją jest: " + currAction.getName() + " - kliknij!");
    } catch (Exception exc) {
      throw new RuntimeException("Nieznana metoda obsługi");
    }
  }

  public void actionPerformed(ActionEvent e) {
    Object src = e.getSource();
    if (src instanceof JMenuItem)
      setCurrentAction(((JMenuItem) src).getText());
    else {
      try {
        currAction.invoke(actionObject); // zmienna liczba arg. !!!
      } catch (Exception exc) {
        JOptionPane.showMessageDialog(null, "Akcja na przycisku nieustalona!");
      }
    }
  }

  public static void main(String args[]) {
    new ReflectionTest();
  }

}

Zobacz działanie programu








W programie korzystamy z klasy Method. Uzyskując odnośnik do konkretnej metody, możemu ustalić ją jako obsługującą zdarzenie (metoda setCurrentAction). Przy zajściu zdarzenia możemy "pośrednio" (dynamicznie) wywołać jego obsługę. Służy temu metoda invoke z klasy Method, użyta w actionPerformed.

Ogólnie:

String name = ....
Class t1 = ....
Class t2 - ...
Method m = c.getDeclaredMethod(name [, t1 [, t2 ...] )

w szczegolności metoda o nazwie name bez argumentów może być uzyskana przez getMethod(name).

m.invoke(trg, x, y, x)



2. JavaBeans

2.1. Pojęcie JavaBean


JavaBean - (bean – ziarno) – to programowy komponent "wielokretnego użytku", którego właściwości i funkcjonalność mogą być odczytywane i/lub zmieniane uniwersalnymi środkami programistycznymi.


Uwaga: JavaBean oznacza zarówno obiekt, jak i klasę tego obiektu. Rozróżnienie zawsze jasno wynika z kontekstu. O JavaBean będziemy mówić krócej "ziarno".

Co oznacza powyższa definicja?
Gdy wbudowujemy zewnętrzny komponent-ziarno do naszego programu, mamy do dyspozycji środki programistyczne, które pozwalają uzyskać o nim informacje:

Uniwersalność sposobów odczytywania i/lub zmieniana charakterystyk obiektu-ziarna opiera się na:

2.2.  Właściwości i akcesory

Ziarna mają właściwości (atrybuty).

Dostęp do własciwości zapewniają metody klasy-ziarna nazywane akcesorami..
Akcesor pobierający właściwości nazywa się getter, a ustalający – setter.


Wyróżniamy własciwości proste (w tym binarne) oraz właściwości indeksowane.
Właściwości proste mają jedną wartość, właściwości indeksowane – wiele wartości, przedstawianych jako tablica.

Standardowe wzorce deklaracji akcesorów są następujące

Dla  prostej (niebinarnej) własciwości o nazwie NNN i typie Typ

getter: Typ getNNN()
setter: void  setNNN(Typ)

np. dla ziarna javax.swing.JButton i własciwości background mamy
getter: Color getBackground()
 i setter: void setBackground(Color).

Dla właściwości binarnej o nazwie NNN:

getter: boolean isNNN()
setter: void setNNN(boolean)

np. boolean isVisible(), setVisible(boolean)

Dla właściwości indeksowanej o nazwie NNN, której wartości reprezentowane są jako tablice elementów typu Typ:
 
getter elementu:  Typ getNNN(int) // zwraca wartość  podanego indeksu właściwości
setter elementu: vois setNNN(int, Typ) // ustala wartość  podanego indeksu właściwości

getter tablicy: Typ[] getNNN()
setter tablicy: void setNNN(Typ[])


Właściwości ziarna mogą być związane (bounded).

O zmianie związanej właściwości ziarna mogą być zawiadamiane inne komponenty i reagować na tę zmianę.

Właściwości ziarna mogą być ograniczane (constrained).
Ograniczana właściwość – to taka, o której zmianie powiadamiane są zainteresowane inne komponenty i są pytane o zgodę na tę zmianę. Jeśli którykolwiek z komponentów nie da takiej zgody (zawetuje zmianę)  – zmiana nie dochodzi do skutku.

Setter związanej i/lub ograniczanej właściwości ma obowiązek wygenerować zdarzenie klasy PropertyChangeEvent..

Klasy-ziarna , mające związane właściwości musżą dostarczyć metody przyłączania słuchaczy zmian właściwości: addPropertyChangeListener(PropertyChangeListener)

Klasy-ziarna, mające ograniczane właściwości, muszą dostarczyć  metody addVetoableChangeListener(VetoableChangeListener).

Ziarna możemy wykorzystywać, możemy też je tworzyć (w znaczeniu: definiować klasę ziarna)

Tworzenie ziarna (jako klasy) wymaga zdefiniowania klasy,  która:

2.3.  Nasłuch i wetowanie zmian właściwosci

Zmiana właściwości związanej lub ograniczanej powinna generować zdarzenie typu PropertyChangeEvent.

Komponenty (obiekty) zainteresowane w śledzeniu zmian  tej własciwości  muszą implementować interfejs PropertyChangeListener.  W ten sposób stają się słuchaczami zmian właściwości..
Komponenty, które mogą wetować zmiany właściwości muszą implementować interfejs VetoableChangeListener (będą więc słuchaczami zmian właściwości ograniczonych i będę miały możliwość wetowania tych zmian).

Zdarzenie typu PropertChangeEvent możemy zapytać o:
Interfejs PropertyChangeListener ma jedną metodę:

    public void propertyChange(PropertyChangeEvent)

W implementacji tej metody, dowiadując się o zmianach właściwości, możemy na nie odpowiednio reagować.

Również interfejs VetoableChangeListener ma jedną metodę: vetoableChange(...) z argumentem–zdarzeniem typu PropertyChange. W jej implementacji , gdy dowiemy się już wszystkich niezbędnych szczegółow o zmianie – możemy ją zawetować.

Wetowanie zmiany  odbywa się na zasadzie zgłoszenia wyjątku PropertyVetoException, zatem deklaracja metody vetoableChange wygląda następująco:

    public void vetoableChange(PropertyChangeEvent e) 
                                                throws PropertyVetoException

a w jej implementacji – gdy po sprawdzeniu warości właściwości chcemy zgłosić veto – sygnalizujemy wyjątek: throw new PropertyVetoException(...).

Słuchacze zmian  właściwości  związanych (jak zawsze w Javie) musżą być przyłączeni do źródła zdarzenia., którym jest  w tym przypadku ziarno. 

Przyłączenie staje się możliwe, jesli w klasie-ziarnie zdefiniowano metodę addPropertyChangeListener(...)
Musi też być zdefiniowana metoda removePropertyChangeListener, odłaczająca słuchacza.

To samo dotyczy nasłuchu zmian właściwości ograniczanych: klasa zaiarno musi dostarczyć  metody przyłączenia słuchacza:  addVetoableChangeListener oraz metody odłączania słuchacza:  removeVetoableChangeListener.

Obowiązkiem klasy ziarna, która implementuje związane i/lub ograniczane właściwości jest również dostarczenie odpowiednich definicji setterów dla tych właściwości.
W setterach należey  generować zdarzenie PropertyChangeEvent  i propagować go pośród przyłączonych słuchaczy.

W pakiecie java.beans znajdują się dwie klasy narzędziowe,  znacznie ułatwiające wykonanie tych zadań: PropertyChangeSupport i VetoableChangeSupport.
Klasy dostarczają metod  generowania zdarzeń i propagacji zdarzeń zmian:
Konstruktory tych klas mają jako argument  referencję do obiektu-ziarna.

Schemat postępowania przy implementacji właściwości związanej jest następujący:

class Ziarno .... {

  //wsparcie
  private PropertyChangeSupport chg = new PropertyChangeSupport(this);

  String text;    // to będzie właściwośc związana o nazwie "text"
  ...
// setter
synchronized void setText(String newTxt) { // pamiętamy o wielowątkowości!

    String oldTxt =  text;   // stara wartość
    text = newTxt;           // ustalenie nowej wartości
     // powiadomienie
    chg.firePropertyChange("text", oldTxt, newTxt);
}
....

// metody dodawania i usuwania słuchaczy
public synchronized void addPropertyChangeListener(PropertyChangeListener l) {
 chg.addPropertyChangeListener(l);
 }

public synchronized void removePropertyChangeListener(PropertyChangeListener l) {
 chg.removePropertyChangeListener(l);
 }
...
}


Implementacja właściwości ograniczanych musi uzwględniać możliwość zawetowania zmiany przez któregoś ze słuchaczy.

Schemat postępowania przy implementacji właściwości ograniczanej jest następujący:

class Ziarno .... {

  //wsparcie
  private VetoableChangeSupport veto = new VetoableChangeSupport(this);

  String text;    // to będzie właściwośc ograniczana o nazwie "tekst"
  ...
// setter

synchronized void setText(String newTxt)  throws PropertyVetoException
 {
    String oldTxt =  text;  // stara wartość

     // wywołujemy metodę fireVotoableChange, która z kolei
     // wywołuje metody vetoableChange zarejestrowanych słuchaczy
     // jeśli któraś z nich zgłasza veto, setter kończy działanie
     // a wyjątek PropertyVetoException jest przekazywany do obsługi
     //  przez metodę wywołującą setText

     veto.fireVetoableChange("tekst", oldTxt, newTxt);

     // Tylko jeśli nikt nie zawetował zmiany:

     text = newTxt;   // ustalenie nowej wartości
}
....
// metody dodawania i usuwania słuchaczy

public synchronized void addVetoableChangeListener(PropertyChangeListener l) {
 veto.addVetoableChangeListener(l);
 }

public synchronized void removeVetoableChangeListener(PropertyChangeListener l) {
 veto.removeVetoableChangeListener(l);
 }
...
}

// Uwaga: wsystkie klasy  zdarzeniowe i interfejsy nasłuchu
// dla właściwości znajdują się w pakiecie java.beans.



2.4.  JavaBean - przykład praktyczny

Będziemy budować klasę-licznik jako JavaBean. Klasę nazwiemy Counter.
Licznik będzie miał jedną właściwość o nazwie count (stan licznika).
W wersji pierwszej uczynimy tę właściwość związaną (bounded), w  wersji drugiej – związaną i ograniczoną (constrained).

Przy budowie aplikacji zastosujemy koncepcję  "Model-View-Controller".

Sama klasa Counter odzwierciedla logikę działania licznika ("model "). Obiekty tej klasy "są niewidzialne", a zatem  żeby zobaczyć licznik musimy stworzyć dodatkową klasę, która zdefiniuje widok licznika  (view).
Nazwiemy ją CounterView. Warto zwrócić uwagę: separacja kodu jest korzystna – widok uniezależniamy od modelu, a model od widoku, w ten sposób możemy mieć np. wiele widoków licznika, lub zmieniać model nie zmieniając widoku.

Komunikacja między modelem i widokiem będzie się odbywać na zasadzie nasłuchu zmian właściwości (zmian właściwości count) czyli obiekt klasy CounterView będzie też słuchaczem zmian właściwości (PropertyChangeListener).

Musimy też mieć jakieś środki zmiany stanu licznika. Interakcję użytkownika z modelem/widokiem zapewnia tzw. kontroler.
Widzieliśmy, że w komponentach Swingu (w naturalny dla nich sposób) kontroler połączony jest z widokiem. Tu jednak odseparujemy jego kod od widoku, tworząc klasę CounterControlGui, zapewniającą interfejs interakcji z licznikiem.. Widok zostanie dodany do tego GUI (ale kody obu klas będą odseparowane).

W wersji drugiej – kiedy właściwość count będzie związana i ograniczana musimy dostarczyć obiektu-nadzorcy, który będzie sprawdzał czy zmiana właściwości jest dopuszczalna i jeśli stwierdzi, że nie – będzie wetował tę zmianę. Odpowiednią klasę nazwiemy CounterLimitator.

W metodzie main(...) klasy Main, w której nasza aplikacja zacznie życie stworzymy wszystkie odpowiednie obiekty w/w klas i ustanowimy niezbędne połączenia między nimi.

W sumie logika działania aplikacji będzie wyglądać tak:


r

Zaczynamy od wersji pierwszej, uboższej, w której klasa Counter daje przykład typowego programowania JavaBean z wlaściwością związaną:

// Klasa Counter

import java.awt.event.*;
import java.beans.*;
import java.io.*;

public class Counter implements Serializable {

  private int count = 0;    // właściwość count

  // Pomocniczy obiekt do prowadzenia listy słuchaczy zmian właściwości oraz
  // propagowania zmian  wśród zarejestrowanych złuchaczy
   private PropertyChangeSupport propertyChange = new PropertyChangeSupport(this);


  // Konstruktory

  public Counter() {
    this(0);
  }

  public Counter(int aCount) {
    setCount( aCount );
  }


  // Metody przyłączania i odłączania słuchaczy zmian właściwości

  public synchronized void addPropertyChangeListener(PropertyChangeListener listener) {
    propertyChange.addPropertyChangeListener(listener);
  }

  public synchronized void removePropertyChangeListener(PropertyChangeListener l) {
    propertyChange.removePropertyChangeListener(l);
  }

  // Proste metody zwiększania i zmniejszania licznika

  public void increment() {
    setCount(getCount()+1);
  }

  public void decrement() {
    setCount(getCount()-1);
  }


  // Getter właściwości "count"
  public int getCount() {
    return count;
  }

  // Setter właściowści "count"
  public synchronized void setCount(int aCount) {
    int oldValue = count;
    count = aCount;

    // wywołanie metody firePropertChange z klasy PropertyChangeSupport
    // powoduje wygenerowanie zdarzenia PropertyChangeEvent i rozpropagowanie
    // go wśród wszystkich przyłączonych słuchaczy, ale tylko wtedy, gdy nowa
    // wartość właściwości różni się od starej wartości

    propertyChange.firePropertyChange("count", new Integer(oldValue),
                                            new Integer(aCount));
  }

}
Widok licznika przedstawimy jako etykietę - klasa ta jednocześnie będzie nasłuchiować zmian właściwości count i odpowiednio do tego zmieniać tekst na etykiecie (a także wyprowadzać informacje o zmianach właściwości count na konsolę).

//Klasa CounterView
//Widok licznika przedstawiamy w postaci etykiety

import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.beans.*;


public class CounterView extends JLabel implements PropertyChangeListener {


  // Konstruktor domyślny: inicjalizuje etykietę tekstem "0"
  CounterView()  {
     this("0");
  }

  // Konstruktor inicjalizujący etykietę podanym tekstem
  CounterView(String lab) {
     super(lab);
     setOpaque(true);   // etykieta nie przezroczysta
       // ramka
     setBorder(BorderFactory.createLineBorder(Color.black));
       // rozmiary i wyrównanie tekstu
     setPreferredSize(new Dimension(75, 40));
     setHorizontalAlignment(CENTER);
  }

  // obługa zdarzenia PropertyChange
  public void propertyChange(PropertyChangeEvent e)  {
    Integer oldVal = (Integer) e.getOldValue(),
           newVal = (Integer) e.getNewValue();
    System.out.println("Value changed from " + oldVal + " to " + newVal);
    setText("" + newVal + "");  // pokazanie na etykiecie nowego stanu licznika
   }


}
Klasa kontrolera - CounterControlGui dostarcza dwóch przycisków (zwiększ, zmniejsz licznik) oraz pole tekstowe, w którym można wpisać wartość licznika (ENTER)
Zarówno kliknięcie w przyciski jak i ENTER na polu tekstowym powoduje powstanie zdarzenia Action, które tu (w tej klasie) obsługujemy ustalając nowe wartości licznika za pomocą metod incremet() decrement() i setCount(...) z klasy  Counter.

import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.beans.*;
import java.io.*;

public class CounterControlGui extends JFrame implements ActionListener {

  Counter counter;
  JButton binc = new JButton("Increment");
  JButton bdec = new JButton("Decrement");
  JTextField txt = new JTextField(10);

  // Konstruktor otrzymuje jako argumenty obiekty typu Counter i CounterView
  // Pierwszy jest nam potrzebny do komunikacji z licznikiem, drugi - widok
  // wbudujemy w to GUI.

  CounterControlGui(Counter c, CounterView clab)  {
    counter = c;
    Container cp = getContentPane();
    cp.setLayout(new FlowLayout());
    binc.addActionListener(this);
    cp.add(binc);
    cp.add(clab);
    bdec.addActionListener(this);
    cp.add(bdec);
    txt.addActionListener(this);
    cp.add(txt);
    setDefaultCloseOperation(3);
    pack();
    show();
  }


  // Obsługa akcji
  public void actionPerformed(ActionEvent e)  {
      if (e.getSource() == txt)  {
         int n = 0;
         try  {
            n = Integer.parseInt(txt.getText());
         } catch (NumberFormatException exc)  { return; }
         counter.setCount(n);
         return;
      }
      String cmd = e.getActionCommand();
      if (cmd.equals("Increment")) counter.increment();
      else if (cmd.equals("Decrement")) counter.decrement();
      else System.out.println("Unrecognized command");
  }

}
I wreszcie lacząca wszystko klasa Main, która inicjuje działanie aplikacji.

public class Main {

 public static void main(String[] args)  {

   // Tworzymy obiekty: licznik i jego widok
   Counter counter = new Counter();
   CounterView counterView = new CounterView(""+counter.getCount());

   // Rejestrujemy widok jako słuchacza zmian licznika
   counter.addPropertyChangeListener(counterView);

   // Tworzymu GUI kontrolera i pokazujemy go
   CounterControlGui gui = new CounterControlGui(counter, counterView);
   gui.pack();
   gui.show();
  }

}

Dizałanie aplikacji ilustruje rysunek oraz komunikaty na konsoli, powstałe po kolejnych klilnięciach w przyciski Incremenet i Decrement oraz wprowadzeniu liczby 23 w polu tekstowym i naciśnieciu ENTER.

r


Value changed from 0 to 1
Value changed from 1 to 0
Value changed from 0 to 23


Wersja 2

W wersji drugiej chcemy dodać obiekt – nadzorcę, który będzie sprawdzał czy zmiana licznika jest dopuszczalna, a jeśli nie to będzie wetował tę zmianę.
Mechanizm wetowania polega na sygnalizowaniu wyjątku PropertyVetoException.
Wyjątek ten będzie sygnalizowany przez obiekt-nadzorcę, który jest jednoczesnie słuchaczem zmian wartości właściwości ograniczonych (VetoableChangeListener).

Klasa definiująca obiekty nadzorujące nasz licznik wygląda tak.

import java.beans.*;

public class CounterLimitator implements VetoableChangeListener {

// minimalne i makszymalne dopuszczalne wartości licznika
private int min, max;

CounterLimitator(int minLim, int maxLim)  {
  min = minLim;
  max = maxLim;
}

// Obsługa zdarzenia vetoableChange
// metoda może sygnalizować PropertyVetoException
public void vetoableChange(PropertyChangeEvent e)
            throws PropertyVetoException {
   Integer newVal = (Integer) e.getNewValue();
   int val = newVal.intValue();
   // Sprawdzamy, czy zmiana  licznika jest dopuszczalna,
   // jeśli nie – sygnalizujemy wyjatek  PropertyVetoException
   if (val < min || val > max)
      throw new PropertyVetoException("Niedopuszczalna zmiana wartości", e);
   }

}

W klasie Counter musimy poczynić zmiany, po to by właściwość count była zarazem związana i ograniczana.
Wykorzystamy podobną do PropertyChangeSupport klasę pomocniczą VetoableChangeSupport
Obiekt tej klasy nazwiemy vetos, dostarczymy też metod przyłączania i odłączania słuchaczy zmian ograniczanych.

public class Counter {

//...

private VetoableChangeSupport vetos = new VetoableChangeSupport(this);

public synchronized void addVetoableChangeListener(VetoableChangeListener l) {
	vetos.addVetoableChangeListener(l);
}

public synchronized void removeVetoableChangeListener(VetoableChangeListener l) {
	vetos.removeVetoableChangeListener(l);
}

// ...
}

Zmianie ulegnie też metoda setCount.

public class Counter {

//...
public synchronized void setCount(int aCount)
                         throws PropertyVetoException {

     int oldValue = count;

     // wywołujemy metodę fireVotoableChange, która z kolei
     // wywołuje metody vetoableChange zarejestrowanych słuchaczy
     // jeśli któraś z nich zgłasza veto, setter kończy działanie
     // a wyjątek PropertyVetoException jest przekazywany do obsługi
     // przez metodę wywołującą setCount
     // (co zaznaczyliśmy w nagłówku metody przez
     // throws PropertyVetoException)

    vetos.fireVetoableChange("count", new Integer(oldValue), new Integer(aCount));

    // tylko jeśli nikt nie zawetował

    count = aCount;  // ustalamy nową wartość licznika

    // ... powiadamiamy nasłuchujących zmiany właściwości count

    propertyChange.firePropertyChange("count", new Integer(oldValue),
                                       new Integer(aCount));
}

//...
}
Ponieważ metody increment, decrement i konstruktory wywołują metodę setCount, to w sygnaturach tych metod i konstruktorów musimy dodać informację, że wyjątek PropertyVetoException ma być obsługiwany przez  wołającego te metody.

A wołamy je z GUI kontrolera.
Tam jest miejsce do obsługi tego wyjątku:
public class CounterControlGui .... {

....
public void actionPerformed(ActionEvent e)  {
  try  {
    if (e.getSource() == txt)  {
       int n = 0;
       try  {
          n = Integer.parseInt(txt.getText());
       } catch (NumberFormatException exc)  { return; }
       counter.setCount(n);
       return;
    }
    String cmd = e.getActionCommand();
    if (cmd.equals("Increment")) counter.increment();
    else if (cmd.equals("Decrement")) counter.decrement();
    else System.out.println("Unrecognized command");
  } catch (PropertyVetoException exc)  { // obługa wyjatku:
      System.out.println(""+ exc);       // podanie informacji
                                         //o niedopuszczalnej zmianie wartości
  }
}

//...
}
W klasie Main dodajemy fragment dotyczący tworzenia obiektu klasy CounterLimitator (nadzorcy) i rejestrujemy go jako słuchacza zmian właściwości ograniczanej:

import java.beans.*;
public class Main {

 public static void main(String[] args) throws PropertyVetoException {
   Counter counter = new Counter();
   CounterView counterView = new CounterView(""+counter.getCount());
   counter.addPropertyChangeListener(counterView);

   // licznik może się zmieniać od –5 do 10
   // bo Limitator zawetuje każdą inną zmianę
   CounterLimitator clim = new CounterLimitator(-5, 10);
   counter.addVetoableChangeListener(clim);

   CounterControlGui gui = new CounterControlGui(counter, counterView);
   gui.pack();
   gui.show();
  }

}
Dzialanie programu  (przy tym samym GUI kontrolera) zilustrujemy komunikatami na konsoli po kolejneych kliknięciach w przycisk "Decrement".



Value changed from 0 to -1
Value changed from -1 to -2
Value changed from -2 to -3
Value changed from -3 to -4
Value changed from -4 to -5
java.beans.PropertyVetoException: Niedopuszczalna zmiana wartości


Jak widać, CounterLimitator nie dopuścił do zmiany wartości z -5 na -6.



2.5. Introspekcja


Srodowisko programistyczne, które ma umożliwiać dynamiczne odczytywanie własności i funkcjonalności dowolnych "podrzucanych" mu ziaren analizuje ziarna za pomocą uniwerslanych metod introspekcji.

Introspekcja kojarzy dwa mechanizmy:
Oba mechanizmy mogą być stosowane łącznie. W prostych przypadkach (prostych ziaren) nie ma potrzeby żmudnego tworzenia klasy BeanInfo, metody refleksji są całkiem wystarczające.
Introspekcja za pomocą metod refleksji jest możliwa dzięki kontraktowi dotyczącemu wzorców nazewnictwa.

Dla właściwości introspekcja określa pary metod get... (is...)  -  set... z tymi samymi nazwami właściwości i z odpowiednimi sygnaturami. Może się okazać, że niektóre właściwości są tylko do odczytu lub tylko do zapisu. Uwzględnia się też indeksowane własciwości.

Dla  określenia, czy istnieją możliwość obsługi zdarzeń i jakich używane są wzorce: addXXXListener i removeXXXListener.

Samą introspekcje realizuje klasa Introspector.

Introspector analizuje klasę-zarna (i nadrzędne wobec niej klasy oraz implementowane interfejsy) zbierając informacje o właściowościach, metodach, zdarzeniach.
Informacja ta jest umieszczana w obiekcie typu BeanInfo (BeanInfo jest nazwą interfejsu). Wobec tego obiektu możemy następnie zastosować metody zwracające iinformacje o ziarnie.

Np. analizę klasy-ziarna javax.swing.JButton uzyskujemy przez następujące odwołanie:

BeanInfo info = Introspector.getBeanInfo(Class.forName("javax.swing.JButton"));
lub
BeanInfo info = Introspector.getBeanInfo(javax.swing.JButton.class));


Następnie wobec obiektu info możemu zastosować metody interfejsu BeanInfo:

Zwracane tablice są tablicami obiektów odpowiednich klas. Wobec tych obiektów stosujemy metody tych klas pozwalające na uzyskiwanie różnej konkretnej informacji.
Np. program na poiniższym wydruku analizuje klasę podaną jako argument i wypisuje niektóre informacje o niej:

import java.lang.reflect.*;
import java.beans.*;

public class BeanAnalyze {

  static void say(String s) { System.out.println(s); }

  public static void main(String[] arg) throws Exception {

    BeanInfo beanInfo = Introspector.getBeanInfo(Class.forName(arg[0]));

    PropertyDescriptor[] pd = beanInfo.getPropertyDescriptors();
    MethodDescriptor[] md = beanInfo.getMethodDescriptors();
    EventSetDescriptor[] evd = beanInfo.getEventSetDescriptors();

    say("Właściwości:");
    for (int i = 0; i < pd.length; i++) {
      say(pd[i].getShortDescription());
      // getReadMethod i getWriteMethod zwracają obiekty typu Method
      say(" getter: "+ pd[i].getReadMethod());
      say(" setter: "+ pd[i].getWriteMethod());
    }

    say("\nMetody:");
    for (int i=0; i<md.length; i++) {
      say(" " + md[i].getMethod());
    }

    say("\nZdarzenia:");
    for (int i = 0; i < evd.length; i++) {
      say("Zdarzenie : " + evd[i].getShortDescription());
      Method[] met = evd[i].getListenerMethods();
      say("Metody obsługi:");
      for (int j=0; j < met.length; j++)  say(" " + met[j]);
    }
  }

}

Fragmenty (dużego!) wydruku z programu uruchomionego z argumentem javax.swing.JButton:


Właściwości:
UI
 getter: public javax.swing.plaf.ButtonUI javax.swing.AbstractButton.getUI()
 setter: public void javax.swing.AbstractButton.setUI(javax.swing.plaf.ButtonUI)
UIClassID
 getter: public java.lang.String javax.swing.JButton.getUIClassID()
 setter: null
accessibleContext
 getter: public javax.accessibility.AccessibleContext javax.swing.JButton.getAccessibleContext()
 setter: null
action
 getter: public javax.swing.Action javax.swing.AbstractButton.getAction()
 setter: public void javax.swing.AbstractButton.setAction(javax.swing.Action)
actionCommand
 getter: public java.lang.String javax.swing.AbstractButton.getActionCommand()
 setter: public void javax.swing.AbstractButton.setActionCommand(java.lang.String)
actionListeners
 getter: public java.awt.event.ActionListener[] javax.swing.AbstractButton.getActionListeners()
 setter: null
actionMap
 getter: public final javax.swing.ActionMap javax.swing.JComponent.getActionMap()
 setter: public final void javax.swing.JComponent.setActionMap(javax.swing.ActionMap)
...

Metody:
 public boolean java.awt.Component.action(java.awt.Event,java.lang.Object)
 public synchronized void java.awt.Component.add(java.awt.PopupMenu)
 public java.awt.Component java.awt.Container.add(java.awt.Component)
 public java.awt.Component java.awt.Container.add(java.awt.Component,int)
 public void java.awt.Container.add(java.awt.Component,java.lang.Object)
 public void java.awt.Container.add(java.awt.Component,java.lang.Object,int)
 public java.awt.Component java.awt.Container.add(java.lang.String,java.awt.Component)
 public void javax.swing.AbstractButton.addActionListener(java.awt.event.ActionListener)
...

Zdarzenia:
Zdarzenie : action
Metody obsługi:
 public abstract void java.awt.event.ActionListener.actionPerformed(java.awt.event.ActionEvent)
Zdarzenie : ancestor
Metody obsługi:
 public abstract void javax.swing.event.AncestorListener.ancestorMoved(javax.swing.event.AncestorEvent)
 public abstract void javax.swing.event.AncestorListener.ancestorAdded(javax.swing.event.AncestorEvent)
 public abstract void javax.swing.event.AncestorListener.ancestorRemoved(javax.swing.event.AncestorEvent)
Zdarzenie : change
Metody obsługi:
 public abstract void javax.swing.event.ChangeListener.stateChanged(javax.swing.event.ChangeEvent)
...


Szczerze mowiąc, za pomocą metod instrospekcji sporo można się dowiedziec o dostępnych właściwościach i metodach danej klasy.

Oczywiście, mając obiekty-metody (uzyskane z deskryptorów metod) mozemy je dynamicznie wyowływac środkami refleksji. Ale istnieje też nieco prostsza, specjalnie dla JavaBean przygotowana możliwość dynamicznego pobierania i ustalania wlaściwości oraz wołania innych metod.


2.6. Dynamiczne pobieranie i ustalanie właściwości

W pakiecie java.beans znajdziemy dwie ciekawe klasy  Statement i Expression

Obiekt klasy Statement tworzymy za pomocą konstuktora:

    Statement s =  Statement( Object target,
                                           String methodName,
                                           Object[] arguments );

a wywołanie na jego rzecz metody void execute() spowoduje wywołanie na rzecz obiektu target metody o nazwie methodName z argumentami podanymi w tablicy arguments

Zwykle stosujemy tę procedure do ustalania właściowości JavaBean, ale może ona być rownież stosowana do wyołania dowolnych metod


Klasa Expression dziedziczy klasę Statement. Czyli tu też możemy wołać metodę execute(). Jednak jej główne zastosowanie polega na dynamicznym pobieraniu właściwości, lub - inaczej - dynamicznym wołaniu metod, które zwracają wyniki i uzyskiwaniu dostępu do tych wyników.

Obiekt klasy Expression tworzymy za pomocą konstuktora:

    Expression e =  Expression( Object target,
                                                String methodName,
                                                Object[] arguments );



Szczegółowo komentowany program ilustruje zastosowanie tych klas.
W poniższym przykładzie metody klas Statement i Expression bedziemy stosowac wobec obiektów klasy JButton oraz naszej własnej klasy TestBean, która wygląda tak:
public class TestBean {

  private String[] headers;
  private int count;


  public TestBean() {
  }

  public TestBean(int n) {
    count = n;
  }

  public String[] getHeaders() {
    return headers;
  }

  public void setHeaders(String[] value) {
    headers = value;
  }

  public int getCount() {
    return count;
  }

  public void setCount(int value) {
    count = value;
  }

}

Szczegółowo komentowany program ilustruje zastosowanie klas Statement i Expression, dając też pewne dodatkowe na ich temat informacje.
import java.beans.*;
import java.awt.*;
import javax.swing.*;

public class DynamicExec {

  public static void main(String[] args) throws Exception {

    Statement stmt;
    Expression expr;

    JButton b = new JButton();

    // Na rzecz przycisku wołamy dynamicznie metodę setText
    // z argumentem "Przycisk"
    stmt = new Statement(b, "setText", new Object[] { "Przycisk" });
    stmt.execute();

    // Jaki wynik? Najpierw statyczne odwołanie
    System.out.println("Tekst na przycisku 1: " + b.getText());

    // Teraz dynamicznie: stwórzmy wyrażenie, którego wynikiem
    // jest wynik podanej metody z podanymi argumentami wywolanej
    // na rzecz b
    // Uwaga: brak argumentów - czyli tablica Object o rozmiarze 0
    expr = new Expression(b, "getText", new Object[0]);

    // Jeżeli wyrażenie expr nie ma jeszcze wyniku
    // metoda getValue() wywołuje podaną w wyrażeniu metodę
    // i zwraca jej wynik; w przeciwnym razie zwraca
    // ustalony wczesniej wynik

    String txt = (String) expr.getValue();
    System.out.println("Tekst na przycisku 2: " + txt);

    // Możemy też stosować klasy Statement i Expression
    // wobec naszych własnych klas JavaBeans

    TestBean tbean = new TestBean();

    // Uwaga: przy przekazywaniu argumentów i zwrocie wynikow
    // następują automatyczne przeksztalcenia pomiedzy
    // typami prostymi i odpowiadającymi im klasami opakowującymi
    // np. int - Integer  - setCount wymaga argumentu int,
    // my podajemy Integer

    stmt = new Statement(tbean, "setCount",
                               new Object[] { new Integer(22) });
    stmt.execute();

    // Jaka jest teraz wartość właściwości count
    // I znowu: getCount() zwraca int, my odbieramy Integer

    expr = new Expression( tbean, "getCount", new Object[0] );
    Integer val = (Integer) expr.getValue();

    System.out.println("Wartość count: " + val);

    // Czy możemy działać na tabliach? Ależ tak!

    stmt = new Statement(tbean, "setHeaders",
                         new Object[] { new String[] { "a", "b" } }
                        );
    stmt.execute();

    expr = new Expression(tbean, "getHeaders", new Object[0]);
    String[] hdr = (String[]) expr.getValue();

    System.out.println("Ustalone nagłówki");
    for (int i=0; i<hdr.length; i++)
       System.out.println(hdr[i]);

    // Możemy nawet stworzyć nowy obiekt
    // używając specjalnej nazwy metody - new  (oczywiście)

    expr = new Expression(TestBean.class, "new",
                          new Object[] { new Integer(111) }
                         );
    TestBean tb2 = (TestBean) expr.getValue();

    expr = new Expression (tb2, "getCount", new Object[0]);
    val = (Integer) expr.getValue();

    System.out.println("W nowym obiecie count = " + val);

  }
}




2.7. Serializacja JavaBeans

Oprócz zapisywania do strumieni obiektowych (ObjectOutputStream), obiekty JavaBeans można serializowac w postaci tekstowej, w formacie XML (wersja 1.0, kodowanie UTF-8). Jest to nawet - w przypadku JavaBeans - bardziej "przenośny" sposób utrwalania obiektów.
Zapisywaniem obiektów zajmuje się klasa XMLEncoder, a ich odtwarzaniem - klasa XMLDccoder. Przy tym  stan obiektu zapisywany jest w postaci  zdatnej do wykorzystania przez klasy Statement i Expression przy odtwarzaniu tego obiektu

Najprostsze (przeznaczone wyłącznie dla JavaBeans) zastosowanie tych klas pokazuje poniższy program.
import java.beans.*;
import java.awt.*;
import javax.swing.*;
import java.io.*;

public class SerialBean {

  String fname = "test.xml";

  public SerialBean() {
    JButton b = new JButton("Kąśliwie wróbel ćwierkał");
    b.setBackground(Color.red);
    b.setForeground(Color.yellow);

    try {
      XMLEncoder enc = new XMLEncoder(
                         new BufferedOutputStream(
                            new FileOutputStream(fname)
                            )
                      );
      enc.writeObject(b);
      enc.close();
    } catch (FileNotFoundException exc) {
        exc.printStackTrace();
        System.exit(1);
    }
    nowReadAndReport();
  }

  private void nowReadAndReport() {
    try {
      XMLDecoder dec = new XMLDecoder(
                          new BufferedInputStream(
                              new FileInputStream(fname)));
      Object obj = dec.readObject();
      JButton b = (JButton) obj;
      dec.close();
      System.out.println("Napis na przycisku: " + b.getText());
      System.out.println("Kolor tła: " + b.getBackground());
      System.out.println("Kolor tekstu : " + b.getForeground());

    } catch (FileNotFoundException exc) {
        exc.printStackTrace();
        System.exit(1);
    }
  }



  public static void main(String[] args) {
    SerialBean serialbean = new SerialBean();
  }


}
Wyprowadzi on na konsolę właściwą  informację o stanie zapisanego a później odtworzonego obiektu:

Napis na przycisku: Kąśliwie wróbel ćwierkał
Kolor tła: java.awt.Color[r=255,g=0,b=0]
Kolor tekstu : java.awt.Color[r=255,g=255,b=0]


Z ciekawości można zajrzeć do pliku test.xml.
<?xml version="1.0" encoding="UTF-8"?>
<java version="1.4.1-rc" class="java.beans.XMLDecoder">
 <object class="javax.swing.JButton">
  <string>KÄĹ>liwie wrĂłbel Ä+wierkaĹ'</string>
  <void property="background">
   <object class="java.awt.Color">
    <int>255</int>
    <int>0</int>
    <int>0</int>
    <int>255</int>
   </object>
  </void>
  <void property="foreground">
   <object class="java.awt.Color">
    <int>255</int>
    <int>255</int>
    <int>0</int>
    <int>255</int>
   </object>
  </void>
 </object>
</java>

Faktycznie, jest do dokładny szablon gotowy do zastosowania klas Expression (dla metody-kostsruktora new z argumentem String) i Statement (dla setBackground i setForeground). Wszystko opiera się na zachowaniu protokołu JavaBeans.

Oczywiście, możemy w ten sposób serializować obiekty prawie wszystkich klas Javy (bo prawie wszystkie są JavaBeans) oraz  własnych klas, spełniających protokół JavaBeans.
A nawet można uwzględnić pewne odstępstwa od tego protokołu (np. inicjację właściwości JavaBeans w konstruktorz, bez użycia setterów) poprzez dostosowanie delegata "persystencji", do którego odwołuje się XMLEncoder - klasy DefaultPersistanceDelegate.
O takim bardziej zaawansowanym zastosowaniu, jak również o użyciu nasłuchu wyjątków dekodowania za pomocą interfejsu ExceptionListener można przeczytać w dokumentacji API Javy.



2.8. Inne zagadnienia związane z JavaBeans


To skrótowe wprowadzenie do JavaBeans nie wyczerpuje tematu.
Warto więc na koniec zwrócić uwagę na nieporuszone tu ważne kwestie
Informacje na te tematy można zanaleźć w dokumentacji.
 

3. Metadane i adnotacje


3.1. Wprowadzenie


Metadane to dane opisujące dane. W przypadku programowania chodzi o takie metadane, które opisują i uzupełniają kod źródłowy w znaczeniu semantycznym


Coraz częściej metadane są obecne w językach programowania.
W Javie częściowo występowały już wcześniej - w samym środowisku (dokumentacyjne np. @author), a częściowo jako zewnętrzne uzupełnienia (np. XDoclet, JBoss AOP).
W C# nazywają się (nieco nieszczęśliwie) atrybutami.

Java 5 rozszerza i standaryzuje zastosowanie metadanych na platformie Javy poprzez mechanizm adnotacji.

Zastosowania (bardzo różnorodne), m.in.
Mechanizm adnotacji jest najważniejszym praktycznie uzupełnieniem Javy w wersji 5, choćby dlatego, że już teraz kolejne wersje dużych platform, takich jak J2EE, a także pakiety narzędziowe , takie jak JDBC 4.0, intensywnie używają adnotacji.


Zalety adnotacji:
Wady:


3.2 Rodzaje adnotacji

Można wyróżnić następujące rodzaje adnotacji:

Adnotacje mogą byś stosowane wobec:

Można wyraźnie zaznaczyć do czego odnosi się nowodefiniowana adnotacja za pomocą metaadnotacji @Target (wtedy inne jej zastosowanie będzie wykryte jako błąd w fazie kompilacji).

Możliwe znaczenia (wartości) metaadnotacji @Target

ANNOTATION_TYPE - dana adnotacja adnotuje inną,
PACKAGE - dotyczy pakietu,
TYPE - klas i interfejsów
METHOD - metod
CONSTRUCTOR - konstruktorów
FIELD - pól
PARAMETER - parametrów
LOCAL_VARIABLE - zmiennych lokalnych


Przykład (fragment) definicji adnotacji o nazwie Test, która może dotyczyć tylko metod:

@Target(ElementType.METHOD)
public @interface Test

Uwagi:
  1. adnotacja nie oznaczona znacznikiem @Target ma zastosowanie wszędzie.
  2. aby określić kilka możliwych zastosowań piszemy @Target({ a, b, c } ), gdzie a, b, c to elementy w postaci ElementType.rodzaj, a rodzaj to jeden z PACKAGE, METHOD, FIELD itp.

Siła adnotacji polega na tym, że mogą one być przetwarzane:

Odpowiadają temu trzy polityki utrzymywania adnotacji, specyfikowane przez metaadnotację @Retention.


Polityki utrzymywania adnotacji

RetentionPolicy.SOURCE - tylko w żródle,
RetentionPolicy.CLASS - w klasie skompilowanej, ale niedostępne w fazie wykonania,
RetentionPolicy.RUNTIME - dostępne w fazie wykonania.


Przykład:
@Retention(RetentionPolicy.SOURCE)   // utrzymanie tylko w kodzie adnotacji AdapterFor
public interface @AdapterFor               // zostanie przetworzona przez narzędzia w fazie
                                                        // kompilacji

Wśrod metaadnotacji dostępne są jeszcze:

@Documented  - mowi o tym, że dokumentacja działania adnotacji ma być włączona do dokumentacji wszystkich oznaczanych przez nią elementów

@Inherited - mówi o tym, że oznaczana przez nią adnotacja (zaznaczająca klasy) ma być dziedziczona przez podklasy zaznaczonych klas.


Zestaw regularnych, wbudowanych w Javę adnotacji jest na razie bardzo niewielki i obejmuje:

@Deprecated - zaznacza dowolny element jako spadkowy ("przestarzały"),

@SupressWarnings - blokuje ostrzeżenia (podanego typu) ze strony kompilatora,

@Override - stosowana  wobec metod, oznacza intencję programisty przedefiniowania metody z nadklasy, dzięki czemu jest możliwość sprawdzenia w fazie kompilacji czy programista nie popełnił błędu,

Ta ostatnia adnotacja jest b. użyteczna i należy ją stosować.
Dzięki temu unikniemy błędów niewykrywalnych nie tylko w fazie kompilacji, ale również czasem w fazie wykonania.

Np.

class Push extends JButton {

    public Dimension getPrefferedSize() { ... }

}

Tutaj nastąpiła pomyłka w nazwie metody - wobec czego pojawia się nowa metoda (nigdy nie wołana), a właściwa (getPreferredSize()) nie jest przedefiniowana. Błędu nie ma  ani w kompilacji ani w fazie wykonania (oprócz - być może nie zawsze, nie od razu, nie w każdych okolicznościach - widocznych niewłaściwych rozmiarów przycisku).

Gdy napiszemy:

class Push extends JButton {

  @Override  public Dimension getPrefferedSize() { ... }



to kompilator wykryje błąd i powiadomi nas o tym.


Jak widać, składnia zastosowania adnotacji jest bardzo prosta.
Adnotacje zaczynają sie znakiem @.
Adnotacje poprzedzają inne kwalifikatory elementów (klas, metod, pól).

3.3. Definiowanie adnotacji

Adnotacje są definiowane jako swego rodzaju interfejsy, za pomocą słowa @interface
Wewnątrz takiego interfejsu dostarcza się deklaracji danych, które adnotacja może zawierać.


[ew. kwalifikacja dostępu]  @interface NazwaAdnotacji {
    deklaracja1
    deklaracja2
     . . .
    deklaracjaN
}


Każda deklaracja ma postać:

 typ nazwaDanej();


albo

typ nazwaDanej() default wartość_domyślna;



Przy zastosowaniu adnotacji możemy podać konkretne dane:

@NazwaAdnotacji(nazwaDanej1=wartość1, nazwaDanej2=wartość2, . . .)

Typy danych w adnotacji mogą być następujące:


Przykład:
public @interface Opis {
    String text() default "Brak opisu";
    int version() default 1;
}


// i zastosowanie np. do opisu klasy:

@Opis(text="Klasa warzyw", version=2)
public class Warzywa { ... }




Można też tak:

@Opis(version=5, text="Klasa warzyw")

albo:

@Opis(text="Klasa warzyw")

tu pominięte dane przyjmą wartości domyślne,
w szczególności:


@Opis
oznacza to samo co

@Opis()
i co

@Opis(text="Brak opisu", version=1)


Mamy też szczególny przypadek, kiedy można pominąć nazwę danych i znak =. Mianowicie:

public @interface JakaśAdnotacja {
   jakiśTyp value()
}


Wtedy można pisać np. tak (jeśli jakiśTyp to int):

@JakaśAdnotacja(111)

nadając danej oznaczanej przez value wartość 111.


Dane konkretnych adnotacji mogą być uzyskiwane od nich (w fazie wykonania lub kompilacji) poprzez odwołania do "metod" interfejsu definiującego adnotację
(np. jeśli annot - jest uzyskaną metodami refleksji lub przez narzędzia przetwarzania w fazie kompilacji adnotacją @Opis, to możemy wołać:

String op = annot.txt();
int v = annot.version();

3.4. Przetwarzanie adnotacji w fazie wykonania

Aby przetwarzać - i odpowiednio stosować - adnotacje w fazie wykonania, należy:

Dla każdego elementu programu (uzyskanego dynamicznie): Class, Method, Field itp. możemy użyć metod, które zwracają informację o adnotacjach zastosowanych wobec tego elementu.

<T extends Annotation>
T
getAnnotation(Class<T> annotationType)
          Zwraca adnotację podanego typu, albu null, jeśli element nie jest oznaczony adnotacją.
 Annotation[] getAnnotations()
          Zwraca wszystkie adnotacje dla tego elementu
 Annotation[] getDeclaredAnnotations()
          Zwraca wszystkie adnotacje bezpośrednio zastosowane (z pominieciem dziedziczonych)
 boolean isAnnotationPresent(Class<? extends Annotation> annotationType)
          Zwraca true, jeśli wobec elementu zastosowano adnotację podanego typu.

Co to jest typ adnotacji?
Nic innego jak nazwa interfejsu który ją definiuje. Np. dla adnotacji, zdefiniowanej jako:

public @interface Opis { ... }

typem jest:

Opis.class
albo
Class.forName("Opis");

Uwagi:

Przykład.

Stworzymy i zastosujemy adnotację, dzięki której w prosty sposób w kodzie źródłowym będziemy ustalać do jakich kontenerów mają być wkładane wybrane komponenty GUI.
Adnotację nazwiemy Loc (od locate).

import java.lang.annotation.*;
import java.awt.*;

@Target(ElementType.FIELD)             // do oznaczania pól
@Retention(RetentionPolicy.RUNTIME)    // faza wykonania

public @interface Loc {
   String to();
}

Adnotacja ma jedną "daną", swoisty atrybut, o nazwie to, który będzie reprezentował za każdym razem nazwę zmiennej oznaczającej kontener do którego dany komponent (oznaczony tą adnotacją)  ma być dodany.

A oto jej zastosowanie:

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import java.lang.reflect.*;

public class Annot0 extends JFrame {

  JComponent cp = (JComponent) getContentPane();

  @Loc(to="cp") JPanel p1 = new JPanel();
  @Loc(to="cp") JPanel p2 = new JPanel();

  @Loc(to="p1") JButton b1 = new JButton("Przycisk 1");
  @Loc(to="p1") JButton b2 = new JButton("Przycisk 2");
  @Loc(to="p2") JButton b3 = new JButton("Przycisk 3");
  @Loc(to="p2") JButton b4 = new JButton("Przycisk 4");
  @Loc(to="p2") JButton b5 = new JButton("Przycisk 5");

  public Annot0() {
    setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    cp.setLayout(new BoxLayout(cp, BoxLayout.Y_AXIS));
    try {
      locateComponents();   // metoda ta zajmie się wkładaniem do kontenerow
    } catch(Exception exc) {
        exc.printStackTrace();
    }
    pack();
    show();
  }
//....
}


Metoda locateComponents() nie jest trudna do napisania, jeśli tylko choć trochę władamy metodami refleksji:

  private void locateComponents() throws Exception {
    Class klasa =  getClass();
    for (Field f : klasa.getDeclaredFields()) {   // po polach klasy
      Loc annot = f.getAnnotation(Loc.class);     // dla f uzyskać anotację Loc
      if (annot == null) continue;                // nie ma - następne pole
      System.out.println(annot);                  // zobaczmy jak wygląda
      String contName = annot.to();               // od adnotacji: nazwa kontenera
      Field contField = klasa.getDeclaredField(contName);  // pole, ktore go deklaruje
      Object container = contField.get(this);  // sam obiekt-kontener
      Method m = container.getClass().getMethod("add", Component.class); // metoda add
      m.invoke(container, f.get(this)); // i jej wywołanie - dodajemy komponent
    }
}
Można powiedzieć, że ten sposób programowania, szczególnie w dużych projektach może być bardzo użyteczny, bowiem łatwo pozwala zmieniać ułożenie komponentów.
Sama metoda locateComponents() może być nieco zmodyfikowana, tak by mogła znaleźć się w jakiejś klasie narzędziowej i być zastosowana wobec dowolnych klas o podobnej j.w. konstrukcji.

3.5. Przetwarzanie adnotacji w fazie kompilacji

Niewątpliwie możliwość przetwarzania adnotacji  w fazie kompilacji  jest najbardziej ekscytująca.
Umożliwia np. generowanie dodatkowych klas czy niezbędnych plików zewnętrznych.

Do takiego przetwarzania powołane są odpowiednie dodatkowe narzędzia.

Należy do nich
apt
czyli "Annotation Processing Tool", dostępny w pakiecie Javy.

Skąd apt ma wiedzieć, jak należy przetwarzać nasze adnotacje?
Otoż musimy mu to sami powiedzieć, dostarczając tzw. procesora adnotacji.


Procesor adnotacji definiujemy implementując we własnej klasie interfejs AnnotationProcessor i dostarczając definicji jedynej jego metody public void process()

Apt uzyskuje dostęp do naszego procesora za pomocą metody getProcessorFor(...) z klasy, którą też musimy zdefiniować i która stanowi fabrykę procesorów - implementację interfejsu AnnotationProcessorFactory.

Podczas wywołania tej metodzie przekazywane jest środowisko działania dla procesora - AnnotationProcessorEnvironment. Z tego środowiska nasz procesor może odczytać wszystkie niezbędne informacje o strukturze kodu źródłowego oraz sposobach tworzenia i generowania nowych plików, a także raportowania błędów i ostrzeżeń.

Apt używa naszego procesora, który np. produkuje dodatkowe pliki, po czym wykonuje wszystkie niezbędna kompilacje.

Uwaga: konieczne są importy pakietów z tools.jar - zob. przykładowy kod źródłowy.


Jako przykład rozpatrzmy prosty sposób zapisu klas typu JavaBeans.
Na podstawie tych bardzo uproszczonych zapisów będą mogły być generowane "prawdziwe" duże klasy JavaBeans. Przykład jest raczej ilustracyjny i do dalszego znaczącego rozszerzania i modyfikowania.

Umówimy się, że na podstawie zapisu:

@BeanTemplate class NazwaBeanaTemplate {
       typ1 nazwa1;
       typ2 nazwa2;
       ....
       tyoN nazwaN;
}

ma być wygenerowana klasa JavaBean o nazwie NazwaBeana, zawierające właściwe deklaracje pól (podanych) oraz odpowiednie dla nich settery i gettery.

Musimy więc mieć adnotację @BeanTemplate:
import java.lang.annotation.*;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public @interface BeanTemplate {
}
 
 i odpowiedni procesor, dostarczany przez naszą implementację fabryki procesorow adnotacji:

import com.sun.mirror.apt.*;
import com.sun.mirror.declaration.*;
import com.sun.mirror.type.*;
import com.sun.mirror.util.*;

import java.beans.*;
import java.io.*;
import java.util.*;


public class BeanTemplateAnnotationFactory
       implements AnnotationProcessorFactory
{

   // Typ adnotacji
   private final String annoType = "BeanTemplate";

   public Collection<String> supportedAnnotationTypes() {
      return Arrays.asList(annoType);
   }

   public Collection<String> supportedOptions() {
      return Arrays.asList(new String[0]);
   }

   public AnnotationProcessor   // ważna metoda getProcessorFor
          getProcessorFor(Set<AnnotationTypeDeclaration> atds,
                          final AnnotationProcessorEnvironment env)
   {
      return new AnnotationProcessor() { // procesor - w klasie wewnętrznej

        public void process() {
          // deklaracje markowane adnotacją annoType (teraz "BeanTemplate")
          Collection<Declaration> dcls =
             env.getDeclarationsAnnotatedWith(
                (AnnotationTypeDeclaration) env.getTypeDeclaration(annoType));

          for (Declaration d : dcls) { 
             if (d instanceof ClassDeclaration) {  // jeżeli klasa
               ClassDeclaration cdcl = (ClassDeclaration) d;
               String name = cdcl.getSimpleName();
               if (!name.endsWith("Template")) {
                  // od env uzyskujemy środki raportowania błędów, ostrzeżeń, info.
                  env.getMessager().printWarning("Wadliwa nazwa klasy bean template");
                  continue;
               }
               String qname = cdcl.getQualifiedName();
               env.getMessager().printNotice(qname);
               qname = qname.substring(0, qname.lastIndexOf("Template"));
               name = name.substring(0, name.lastIndexOf("Template"));
               try {
                 // od env uzyskamy plik i skojarzony z nim PrintWriter
                 PrintWriter out = env.getFiler().createSourceFile(qname);
                 // generujemy świeży kod na podstawie info o zaznaczonej klasie
                 out.println("public class " + name + " {" );
                 Collection<FieldDeclaration> fdcl = cdcl.getFields();
                 for (FieldDeclaration f : fdcl) {
                   String fname = f.getSimpleName();
                   String mname = Character.toUpperCase(fname.charAt(0)) +
                                  fname.substring(1);
                   String ftype = f.getType().toString();
                   out.println("  private " + ftype + " " + fname + ";");
                   out.println("  public " + ftype + " get" +  mname +
                               "() { return " + fname + "; }");
                   out.println("  public void set" +  mname +
                               "(" + ftype + " v) { " + fname + " = v; }");
                 }
                 out.println("}");
                 out.close();
               } catch(IOException exc) { exc.printStackTrace(); }
             }
             else  // jeśli nie byla klasa, to ktoś się pomylił i zazn. interfejs
               env.getMessager().printWarning("Adnotacja dotyczy interfejsu");
          }
        }
      };
   }



}

Po skompilowaniu tej fabryki umieszczamy wynik kompilacji w katalogu, w którym znajdują się  inne pliki źródłowe. Np. takie przykładowe zastosowanie:

Uproszczona definicja beana:
import java.awt.*;
@BeanTemplate public class Bean1Template {
  String txt;
  Color color;
}
Ponieważ, zgodnie z umową ma z tego powstać prawdziwa klasa JavaBean, to plik ją wykorzystujący będzie się odwoływał do Bean1, a nie Bean1Template:

import java.awt.*;
public class Annot1 {


  public Annot1() {
    Bean1 b = new Bean1();
    b.setTxt("Pies");
    b.setColor(Color.BLUE);

    System.out.println(b.getTxt() + "\n" + b.getColor());

  }

  public static void main(String[] args) {
     new Annot1();
  }

}

Aby to wszystko ze sobą polaączyć, musimy wywołać apt, podając mu lokalizację fabryki procesorow:

apt -factory BeanTemplateAnnotationFactory *.java

APT nie tylko przeprowadzi analizę adnotacji i odpowiednie ich substytucje, nie tylko skorzysta z naszego procesora i pozwoli mu zapisać nowy plik źródłowy, ale również wywoła normalny kompilator javy, aby wszystko złożył do kupy.

W efekcie uzyskamy nowowygenerowany plik źródłowy (który "zastępuje" uproszczony "template"):
public class Bean1 {
  private java.awt.Color color;
  public java.awt.Color getColor() { return color; }
  public void setColor(java.awt.Color v) { color = v; }
  private java.lang.String txt;
  public java.lang.String getTxt() { return txt; }
  public void setTxt(java.lang.String v) { txt = v; }
}
a w wyniku kompilacji wszystkiego razem działający plik Annot1.class, który wyprodukuje napis:

Pies
java.awt.Color[r=0,g=0,b=255]


Zobacz multimedialną prezentację użycia APTr

Podany przykład jest prosty, a także trochę dyskusyjny. Ale dobrze pokazuje potencjalnie olbrzymie możliwości tkwiące w przetwarzaniu adnotacji w fazie kompilacji.


3.6. Adnotacje a transformowanie kodu binarnego


Polityka utrzymywania adnotacji oznaczana przez metaadnotację @Retention jako RetentionPolicy.CLASS utrzymuje adnotację w klasie (kodzie binarnym), ale nie udostępnia jej mechanizmom refleksji.

Taki typ adnotacji może być wykorzystywany przez różne narzędzia modyfikacji kodu binarnego klas. Należą do nich takie narzędzia jak:

Javassist pozwala na bardzo łatwe transformowanie kodu binarnego klas, polegające m.in na:
Modyfikacje są łatwe, ponieważ możemy zapisać je w naturalnym języku Javy.
Javassist ma pewne ograniczenia.
Znacznie potężniejszym narzędziem jest BCEL (Byte Code Engineering Library), który pozwala robić praktycznie wszystko, ale wymaga zapisów w postaci podobnej do bajtkodu.

Zobacz więcej o:
Javassist - http://jboss.com/products/javassist
BCEL - http://jakarta.apache.org/bcel/

4. Skrypty w Javie

4.1. Wprowadzenie


W Javie 6 wbudowano możliwość uruchamiania skryptów z poziomu aplikacji. Naturalnie, taka możliwośc istniała zawsze, ale teraz mechanizmy i interfejsy zostały ustandaryzowane, a w pakietach Javy mamy javax.script, dodstarczający gotowych klas i interfejsów do wykorzystania w tym celu.

Co może robić aplikacja Javy ?

Co może robić skrypt ?

Jakie są tego zastosowania ?

4.2. Motory skryptowe i dostęp do nich z aplikacji Javy

Motor skryptowy (script engine) - to komponent software'wy, który wykonyje programy napisane w języku skryptowym. Wykonanie polega na interpretacji, składającej się z następujących faz: parsowanie kodu,  utworzenie tablicy symboli do przechowywania wartości, właściwe wykonanie. Motor skryptowy nazywany jest także interpreterem


Java Script Engines (JSE) - to motory skryptowe zrealizowane jako moduły napisane w Javie (pliki .jar) i eksponujące jednolity interfejs programistyczny, zgodny ze specyfikacją JSR-223.
Oczywiście, mogą one używac odwołań do funkcji rodzimych, a zatem mogą "powierzać" własciwe wykonanie skryptu interpreterom, które mają odpowiednie API do takiego współdziałania.

W bibliotekach Javy 1. 6  dostępny jest JSE dla języka JavaScript w wersji Rhino.
Dodatkowe motory skryptowe są dostępne na stronie: https://scripting.dev.java.net/.
Mogą powstawać (i powstają) nowe motory skryptowe.

Tabela przedstawia wybrane motory skryptowe

Language Description Implementation
AWK AWK is a general purpose language that is designed for processing text-based data, either in files or data streams. Jawk is Java-like, Awk-like reporting language. Jawk
BeanShell BeanShell is a small, free, embeddable Java interpreter with object scripting language features, written in Java. BeanShell dynamically executes standard Java syntax and extends it with common scripting conveniences such as loose types, commands, and method closures like those in Perl and JavaScript. BeanShell 2.0b5
ejs "ejs" (Embedded JavaScript) is JSP-like templating engine for JavaScript. It supports the usual <%= expr %>, <% code %> syntax. This engine is completely implemented in JavaScript. You need to use JavaScript engine to use this script engine. Implementation contained in one JavaScript file.
FreeMarker FreeMarker is a Java-based general purpose template engine. FreeMarker 2.3.8
Groovy Groovy is an agile dynamic language for the Java 2 Platform that has many of the features that people like so much in languages like Python, Ruby and Smalltalk, making them available to Java developers using a Java-like syntax. Groovy 1.0 jsr-06
Jaskell Jaskell is a lazy functional programming language. It stands for "Java Haskell". It features higher-order functions, function currying, string interpolation, lazy evaluation, dynamic typing. Jaskell 1.0
Java http://java.sun.com Java Compiler API (JSR 199)
JavaScript Web Browser's native JS interpreter is wrapped as javax.script API. Note that this script engine works only under web browsers. i.e., only within Java applets. Web Browser's JS implementation (tested with Firefox 1.5.0)
Jelly Jelly is a tool for turning XML into executable code. So Jelly is a Java and XML based scripting and processing engine. Jelly borrows many good ideas from both JSP custom tags, Velocity, Cocoon, Ant. In this script engine, <script> tag has been added. Any JSR-223 compliant language may be used. Also, expressions [${xxx}] may be from any scripting language rather than Jexl alone. Jelly 1.0
JEP JEP is a Java library for parsing and evaluating mathematical expressions. JEP supports BigInteger, BigDecimal, complex, Vector/Matrix/Tensor arithmetic. JEP (Java Math Expression Parser) 2.4.0
Jexl Java Expression Language (JEXL) is an expression language engine which can be embedded in applications and frameworks. JEXL is inspired by Jakarta Velocity and the Expression Language defined in the JavaServer Pages Standard Tag Library version 1.1 (JSTL) and JavaServer Pages version 2.0 (JSP). Jexl 1.0
jst "jst" (JavaScript Templates) is JSP/ASP/PHP-like templating engine for JavaScript. This engine uses TrimPath's JavaScript Templates implementation. This script engine is implemented in JavaScript. You need to use JavaScript engine to use this script engine. TrimPath JavaScript Templates (1.0.38)
JudoScript A scripting language built atop Java, and is a powerful general-purpose programming language with intimate Java scripting support. JudoScript, or Judo for short, is a general-purpose, Java scripting and multi-domain language. A full-fledged general-purpose scripting language with full capability of Java scripting, JudoScript intimately supports most of today's key computing areas. JudoScript 0.9
OGNL OGNL stands for Object-Graph Navigation Language; it is an expression language for getting and setting properties of Java objects. You use the same expression for both getting and setting the value of a property OGNL 2.6.9
Pnuts Pnuts is a simple but powerful scripring language that is embeddable into Java applications. It has an extensible module system and a lot of ready-to-use modules. Pnuts 1.1
Python Python is a dynamic object oriented programming language that can be used for many kinds of software development. It offers strong support for integration with other languages and tools, comes with extensive standard libraries, and can be learned in a few days time. Jython 2.1
Ruby Ruby is the interpreted scripting language for quick and easy object-oriented programming. It has many features to process text files and to do system management tasks (as in Perl). It is simple, straight-forward, extensible, and portable. JRuby 0.9.0
Scheme Scheme is a statically scoped and properly tail-recursive dialect of the Lisp programming language invented by Guy Lewis Steele Jr. and Gerald Jay Sussman. It was designed to have an exceptionally clear and simple semantics and few different ways to form expressions. A wide variety of programming paradigms, including imperative, functional, and message passing styles, find convenient expression in Scheme. SISC 1.15.3
Sleep Sleep is a Java-based scripting language heavily inspired by Perl. Sleep 2.0
Tcl Tcl (Tool Command Language) is a very powerful but easy to learn dynamic programming language, suitable for a very wide range of uses, including web and desktop applications, networking, administration, testing and many more. Open source and business-friendly, Tcl is a mature yet evolving language that is truly cross platform, easily deployed and highly extensible. Jacl 1.3.3
Velocity Velocity is a Java-based general purpose template engine. Velocity 1.4
XPath JDK already includes javax.xml.xpath package for XPath. But to extend XPath with user defined functions and variables, user needs to learn different set of interfaces/classes in this (and few other) package(s). We are adding a javax.script engine on top of javax.xml.xpath API - so that user can set variables, functions in ScriptContext and call "eval" method on ScriptEngine. Any Java method, constructor may be used as XPath extension function - no need to wrap it as "XPathFunction". JDK implementation of javax.xml.xpath is used.
XSLT JDK already includes javax.xml.transform package for XSLT. But to use XSLT, user needs to learn different set of interfaces/classes in this (and few other) package(s). We are adding a javax.script engine on top of javax.xml.transform API - so that user can set source, result in ScriptContext and call "eval" method on ScriptEngine. By default, ScriptContext's input Reader and output Writer are used for transform source and result. JDK implementation of javax.xml.transform is used.
Wszystkie w/w JSE są dostępne z w/w strony jako archiwum katalogów sr223-engines.zip.

Dodatkowe JSE wg specyfikacji JSR 223: 
Aby JSE był dostępny z poziomu aplikacji Javy jego JAR musi być widoczny dla ClassLoadera (np. umieszczony na ścieżce CLASSPATH.

Dostęp do JSE z poziomu aplikacji Javy jest realizowany za pośrednictwem zarządcy motorów -  ScriptEngineManager. Zajmuje się on m.in.  wyszukiwaniem i instancjacją odpowiednich JSE.

Do odnajdywania JSE służą następujące metody klasy ScriptEngineManager.

 ScriptEngine
getEngineByName(String shortName)
          Wyszukuje i tworzy JSE dla podanej nazwy motoru (lub jej aliasu)
 ScriptEngine getEngineByExtension(String extension)
          Wyszukuje i tworzy JSE dla podanego rozszerzenia plików skryptowych.
 ScriptEngine getEngineByMimeType(String mimeType)
          j.w. - tylko dla  podanego typu MIME



Uzyskanie JSE:

ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName(nazwa_motoru);

A jakie są nazwy?
To oczywiście podaje dokumentacja konkretnych JSE. Ale możemy się tego sami szybko dowiedzieć. Sprawdzenie nazw jest użyteczne wtedy, gdy nazwy się powtarzają dla różnych JSE (np. javascript) i wtedy trzeba zastosowac rozróżniające aliasy.

Metainformacje o JSE uzyskujemy za pomocą interfejsu ScriptEngineFactory.
Tak naprawdę ScriptEngineManager odnajduje właśnie odpowiednie fabryki dla danych JSE i za pomocą ich metod fabrycznych tworzy obiekty ScriptEngine.

Uwaga: ScriptEngineManager stosuje mechanizm service provider zob.

http://java.sun.com/javase/6/docs/technotes/guides/jar/jar.html#Service%20Provider

do wykrywania fabryk.

Wymaga to aby JARy JSE w katalogu META-INF  zawierały podkatalog services z plikiem o nazwie javax.script.ScriptEngineFactory, zawierającym nazwę klasy będącej fabryką tworzącą obiekty danego JSE. Np. groovy-engine.jar w tym pliku zawiera nazwę com.sun.script.groovy.GroovyScriptEngineFactory. 


Wszystkie znane zarządzcy fabryki można uzyskać za pomoca metody:

ScriptEngineManager mgr ...
List<ScriptEngineFactory> factories = mgr.getEngineFactories();

Przykładowy program pokazuje dostępne informacje.

import java.util.*;
import javax.script.*;

public class DiscoverEngines {
  
  public static void main(String[] args) {

    ScriptEngineManager mgr = new ScriptEngineManager();
   
    List<ScriptEngineFactory> factories = mgr.getEngineFactories();
    
    for (ScriptEngineFactory factory : factories) {
    
      System.out.println("ScriptEngineFactory Info");
      
      String engName = factory.getEngineName();
      String engVersion = factory.getEngineVersion();
      System.out.println("Script Engine: " + engName + " v. " + engVersion);

      List<String> engNames = factory.getNames();
      for(String name : engNames) System.out.println("Engine Alias: " + name);
      
      String langName = factory.getLanguageName();
      String langVersion = factory.getLanguageVersion();
      System.out.println("Language: " + langName + " v. " +  langVersion);
      
      List<String> exts = factory.getExtensions();
      for(String ext : exts) System.out.println("Script file extension: " + ext);
      
      System.out.println("-------------------------------------------");
    }    
  }

}
W przypadku gdy na ścieżce classpath  znajdują się JSE dla browserjs i groovy otrzymamy wynik:


ScriptEngineFactory Info
Script Engine: groovy v.
Engine Alias: groovy
Language: groovy v. 1.0
Script file extension: groovy
-------------------------------------------
ScriptEngineFactory Info
Script Engine: Browser JavaScript Engine v. 1.5
Engine Alias: js
Engine Alias: javascript
Engine Alias: JavaScript
Engine Alias: ecmascript
Engine Alias: ECMAScript
Engine Alias: BrowserJS
Engine Alias: NativeJS
Language: JavaScript v. 1.5
Script file extension: js
-------------------------------------------
ScriptEngineFactory Info
Script Engine: Mozilla Rhino v. 1.6 release 2
Engine Alias: js
Engine Alias: rhino
Engine Alias: JavaScript
Engine Alias: javascript
Engine Alias: ECMAScript
Engine Alias: ecmascript
Language: ECMAScript v. 1.6
Script file extension: js
-------------------------------------------

4.2. Wykonanie skryptów

Skrypty traktowane są jako ciągi znaków:

Skrypt może być wywołany jako całość lub też - jesli język skryptowy to dopuszcza - może być wywołana wybrana funkcja lub metoda ze skryptu.

Do wykonania całego skryptu służy metoda eval wołana na rzecz obiektu typu ScriptEngine.



ScriptEngine eng  = ...;
eng.eval( skrypt);

gdzie:
    skrypt - jest typu String lub typu Reader

Uwaga: przy wywołaniu eval nalezy obsługiwac wyjątek ScriptException


Przykład 1.

import javax.script.*;


public class GroovyTest {

  public static void main(String[] args) {
    ScriptEngineManager manager = new ScriptEngineManager();
    ScriptEngine engine = manager.getEngineByName("groovy");
    try {
      String script = "\"Ala ma kota i psa\".tokenize().each { println it + ' ' + it.length()}";
      engine.eval(script);
    } catch (ScriptException e) {
      e.printStackTrace();
    }
  }

}
Uwagi:
  1. treśc skryptu sformułowano w języku Groovy jako String
  2. warto zwrócić uwagę na moc tego języka - metoda tokenize(), konstrukcja each oraz tzw. domknięcie (zamiast definiowania tradycyjnej funkcji) bardzo ułatwia programowanie.

Wynik:

Ala 3
ma 2
kota 4
i 1
psa 3


Skrypt można wczytać też bezpośrednio z pliku:

import java.io.*;
import javax.script.*;

public class GroovyTest2 {

  public static void main(String[] args) {
    ScriptEngineManager manager = new ScriptEngineManager();
    ScriptEngine engine = manager.getEngineByName("groovy");

    try {
      engine.eval(new FileReader("test1.groovy"));
    } catch (ScriptException e) {
      e.printStackTrace();
    } catch (FileNotFoundException e) {
      e.printStackTrace();
    }
  }

}


Wywołanie określonej funkcji lub metody - zdefiniowanej w  skrypcie - jest możliwe wtedy gdy ScriptEngine implementuje interfejs Invocable. Należy to zawsze sprawdzić, a następnie dokonać konwersji do Invocable i wywolac metodę invokeFunction lub invokeMethod.

 Object invokeFunction(String name, Object... args)
          Woła funkcje name zdefiniowaną w skrypcie z argumentami args i zwraca wynik w postaci Object. Konwersje z typów wyniku funkcji do typów Javy są zalezne od implementacji języka skryptowego.
Przy braku funkcji zgłasza wyjątek NoSuchMethodException. Skrypt powinien być wczesniej przygotowany przez wywołanie eval.
 Object invokeMethod(Object obj, String name, Object... args)
          Wywołuje metodę name na rzecz obiektu obj z argumentami args.

Przykład.

W pliku test1.js mamy następujący zestaw funkcji:

function square(n) {
  return n*n;
}

function cube(n) {
  return n*n*n;
}
Będziemy je wywoływac z aplikacji Javy podając nazwę funkcji i argument:

import java.io.*;
import javax.script.*;
import javax.swing.*;

public class InvokeTest {

  public static void main(String[] args) {
    ScriptEngineManager manager = new ScriptEngineManager();
    ScriptEngine engine = manager.getEngineByName("rhino");

    if (!(engine instanceof Invocable))
      throw new RuntimeException("Engine not invocable");

    Invocable eng = (Invocable) engine; // konieczna konwersja

    try {
      engine.eval(new FileReader("test1.js")); // przygotowanie skryptu
      String in;
      while ((in = JOptionPane.showInputDialog("Podaj nazwe funkcji i argument")) != null) {
        String[] call = in.split(" ");
        double res = (Double) eng.invokeFunction(call[0], call[1]); // wołanie wybranej funkcji
        System.out.println("Wynik " + res);
      }
    } catch (ScriptException e) {
      e.printStackTrace();
    } catch (FileNotFoundException e) {
      e.printStackTrace();
    } catch (NoSuchMethodException e) {
      e.printStackTrace();
    }
  }

}


Skrypty - o ile dany JSE to dopuszcza - mogą być także kompilowane (do symbolicznej postaci) co przyspiesza wielokrotne wykonanie.  W tym przypadku:

4.3. Wymiana danych

Wymiana danych pomiędzy aplikacją i skryptem odbywa się poprzez zestawy par: klucz - wartość, zwane  wiązaniami (bindings). W tych parach kluczem jest nazwa zmiennej, a wartością - wartość zmiennej.
Wiązania są obiektami klas implementujących interfejs Bindings. Interfejs ten rozszerza interfejs Map<String, Object>, zatem wiązania mogą być traktowane  generalnie tak jak mapy (np. dodawanie za pomocą put, pobieranie za pomocą get).
Wiązania mogą być następujących rodzajów:


Zakresy są zarządzane przez ScriptContext (który oprócz tego daje dostęp do strumieni we-wy i błędów JSE).

Co jest w wiązaniach i jak z nimi postępować?

Automatycznie wszystkie zmienne utworzone w trakcie wykonania skryptu (w skrypcie), są dodawane do wiązania zakresu SCRIPT_ENGINE (lub jeśli w eval podano argument Bindings - do tego właśnie wiązania). W aplikacji możemy:

Zmienne aplikacji Javy możemy dodawać do wiązań za pomocą metod:
Te zmienne będą bezpośrednio dostępne w skrypcie pod nazwami = kluczom.

Przykłady (JSE = Rhino):

....
      String script = "a =1; b = 2; c =2;";
      engine.eval(script);
      showBindings();
....


  private static void showBindings() {
    System.out.println("Show bindings");
    ScriptContext ctx = engine.getContext();   // uzyskanie biezącego kontekstu
    List<Integer> scopes = ctx.getScopes();    // z tego kontekstu - jakie są zakresy
    for (Integer scope : scopes) {             // dla każdego zakresu.... :
      System.out.println("Scope: " + scope);
      Bindings bnd = ctx.getBindings(scope);            // wiązania
      System.out.println(bnd.getClass().getName());     // jaka to klasa?
      for (String key : bnd.keySet()) {                 // co jest w wiązaniach?
        System.out.println(key + " = " + bnd.get(key));
      }
    }
  }
Wynik:

Show bindings
Scope: 100                             // <--- to jest ENGINE_SCOPE
javax.script.SimpleBindings     // wiązania są klasy SimpleBindings
b = 2.0                                  // zmienna ze skryptu
c = 2.0                                  // zmienna ze skryptu
println = sun.org.mozilla.javascript.internal.InterpretedFunction@dd5b
a = 1.0                                  // zmienna ze skryptu
context = javax.script.SimpleScriptContext@c4bcdc
print = sun.org.mozilla.javascript.internal.InterpretedFunction@4b4333
Scope: 200                            // <--- zakres GLOBAL_SCOPE
javax.script.SimpleBindings    // nic w nim nie ma bo nic nie dodaliśmy


Wiązania zakresu ENGINE_SCOPE trwają wraz z motorem - kolejne skrypty mogą je uzupełniać:

      String script = "a =1; b = 2; c =2;";
      engine.eval(script);
      script = "xyz = 10;";
      engine.eval(script);
      showBindings();
showBindings pokaże m.in.


b = 2.0
c = 2.0
a = 1.0
xyz = 10.0


Łatwo możemy przekazac zmienną do skryptu:

      String txt = "Ala ma kota";
      engine.put("txt", txt);
      script = "println('Ze skryptu : ' + txt + a + b + c + xyz);";
      engine.eval(script);
Dostaniemy (txt pochodzi z aplikacji, zmienne a, b, c, xyz z wykonań poprzednich skryptów :

L100
Ze skryptu : Ala ma kota12210


Możemy użyć własnych Bindings:
      Bindings sb = new SimpleBindings();
      int i = 1000;
      Date data = new Date();
      sb.put("i", i);
      sb.put("data", data);
      script = "println('Ze skryptu : ' + data + ' liczba ' + i); hhh = 7777;"; 

      engine.eval(script, sb);  // podajemy jako drugi argument Bindings

      showBindings();
      System.out.println("A co jest w naszych bindings?");
      for (String key : sb.keySet()) {
        System.out.println(key + " = " + sb.get(key));
      }
      System.out.println("Koniec naszych");
Wynik:

Ze skryptu : Tue Oct 10 09:35:44 CEST 2008 liczba 1000
Show bindings
Scope: 100
javax.script.SimpleBindings
b = 2.0
c = 2.0
println = sun.org.mozilla.javascript.internal.InterpretedFunction@15dfd77
a = 1.0
context = javax.script.SimpleScriptContext@c4bcdc
txt = Ala ma kota
print = sun.org.mozilla.javascript.internal.InterpretedFunction@1abc7b9
xyz = 10.0
Scope: 200
javax.script.SimpleBindings
A co jest w naszych bindings?
hhh = 7777.0
println = sun.org.mozilla.javascript.internal.InterpretedFunction@1621e42
context = javax.script.SimpleScriptContext@b09e89
data = Tue Oct 10 09:35:44 CEST 2006
print = sun.org.mozilla.javascript.internal.InterpretedFunction@1787038
i = 1000
Koniec naszych

Tutaj widać, że domyślny ENGINE_SCOPE nie jest zmieniany,  w przekazanych wiązaniach znajdziemy dodane przez nas zmienne (i mogą one być użyte w skrypcie), dodatkowo znajdą się tam  zmienne opisujące kontekst (contezt, println, print).

Okazuje się, że nasze wiązania zastępują domyślny ENGINE_SCOPE i dlatego w poniższym fragmencie zmienna txt nie jest znana (chociaż jest w wiązaniach zakresu ENGINE):
      script = "println('Ze skryptu liczba = ' + i); print('Ze skryptu txt: '); println(txt);";
      engine.eval(script, sb);
Wynik:

Ze skryptu liczba = 1000
Ze skryptu txt: javax.script.ScriptException: sun.org.mozilla.javascript.internal.EcmaError: ReferenceError: "txt" is not defined. (<Unknown source>#1) in <Unknown source> at line number 1


Naturalnie, możemy łączyć wiązania:

      Bindings eb = engine.getBindings(ScriptContext.ENGINE_SCOPE);  // wiązania motoru
      eb.putAll(sb);  // dodajemy do nich nasze
      script = "println('Ze skryptu : ' + txt + ' liczba ' + i);";
      engine.eval(script);
      showBindings();
Teraz będą znane zmienne zarówno z zakresu ENGINE jak i z naszych Bindings.


Ze skryptu : Ala ma kota liczba 1000
Show bindings
Scope: 100
javax.script.SimpleBindings
hhh = 7777.0
b = 2.0
c = 2.0
println = sun.org.mozilla.javascript.internal.InterpretedFunction@fa9cf
a = 1.0
data = Tue Oct 10 10:08:19 CEST 2006
context = javax.script.SimpleScriptContext@c4bcdc
txt = Ala ma kota
print = sun.org.mozilla.javascript.internal.InterpretedFunction@55571e
i = 1000
xyz = 10.0


Nalezy zwrócić uwagę, że Bindings dzialają tak jak każda mapa. Nie możemy liczyć na to, że po dodaniu zmiennej i oraz txt do Bindings, wykonaniu skryptu i zmianie wartości zmiennych ponowne wykonanie skryptu dostrzeże te zmiany:

      i = 99;
      txt = "Nowy tekst";
      engine.eval(script);
L100
Ze skryptu : Ala ma kota liczba 1000


Oczywiście, w mapie są referencje (jako wartości), ale przecież przy dodawaniu zmiennych typów prostych do mapy następuje boxing (i refrencja wskazuje na wtedy utworzony obiekt). To samo dotyczy zmiennej txt - referencja w mapie wskazuje na napis "Ala ma kota"  (a nie na nowy napis "Nowy tekst").
Wyjściem z sytuacji jest albo użycie obiektów klas modyfikowalnych albo ponowne ładowanie mapy Bindings po zmianie wartości na poziomie aplikacji.

Uwaga: jako kluczy oznaczających nazwy zmiennych (lub inną informację przekazywaną pomiędzy skryptem i aplikacją) nie wolno używać nazw zarezerwowanych, które zaczynają się od javax.script. Obecnie zdefiniowane są następujące klucze zarezerwowane.


javax.script.argvZobacz w dokumentacji znaczenie podanych zmiennych
javax.script.filename
javax.script.engine
javax.script.engine_version
javax.script.language
javax.script.language_version


4.4. Kontekst


Wspomniany już ScriptContext zapewnia łączność pomiędzy aplikacją i skryptem.
Każdy skrypt wykonywany jest w jakimś kontekście (w szczególności - domyślnym).
Możemy zmienić kontekst domyślny, podając własny kontekst w metodzie eval:
eval(skrypt, ScriptContext)

Możemy też modyfikowac domyślny kontekst.

Oprócz wiązań (zakresów) - kontekst zapewnia dostęp do strumieni wejścia, wyjścia oraz błędów dla skryptu.

Poniższy fragment pokazuje jak można przechwycić wyjście skryptu:

      // Zobaczmy co można zrobić z Writerem
      StringWriter sw = new StringWriter();
      engine.getContext().setWriter(new PrintWriter(sw));
      script = "println('To powinno pójść na nowy Writer');";
      engine.eval(script);

      JOptionPane.showMessageDialog(null, sw.toString());
W rezultacie pokaże się okienko dialogowe (komunikatów) z napisem "To powinno pójść na nowy Writer".

4.5. Dostęp do obiektów Javy ze skryptów


Motory skryptowe zgodne ze specyfikacją JSR-223 umożliwiają w skryptach dostęp do klas z pakietów Javy.
Szczegóły (w tym składnia wywołania konstruktorów i  metod) zależne są od implementacji języka skryptowego.
Generalnie poslugujemy się  kwalifikowanymi nazwami klas (np. javax.swing.JFrame).
W językach, które umożliwiają dodanie do przestrzeni nazw nazw pakietów i klas Javy można stosowac konstrukcję typu import.

Oczywiście, możemy też odwoływać się do naszych własnych klas - do ich publicznych metod.

Przykłady.
public class SwingFromRhino {

  public static void main(String[] args) {
    ScriptEngineManager manager = new ScriptEngineManager();
    ScriptEngine engine = manager.getEngineByName("rhino");
    
    String script = engine.getFactory().getProgram(
        "importPackage (javax.swing)",
        "f = new JFrame('Okno')",
        "f.setSize(200,200)",
        "f.show()"
        );
    
    System.out.println(script);
    
    try {
      engine.eval(script);
    } catch (ScriptException e) {
      e.printStackTrace();
    }

  }

}
Warto zwrócić uwagę na metodę z getProgram(String ...) interfejsu ScriptEngineFactory, która zwraca gotowy do wykonania  program, składający się z indtrukcji podanych jako argumenty, zgodnie ze składnią danego języka skryptowego.

W tym kontekście warto swpomnieć też o innych użytecznych metodach pomocniczych interfejsu ScriptEngineFactory:
 String getMethodCallSyntax(String obj, String m, String... args)
          Zwraca napis, który może być użyty w danym języku skryptowym do wywolania metody klasy Javy na rzecz obiektu 
 String getOutputStatement(String toDisplay)
          Zwraca napis, który może być użyty jako instrukcja danego języka skryptowego do wyprowadzenia napisu toDisplay


Przykład.
class Person {
  String name;

  public Person(String name) {
    super();
    this.name = name;
  }


  public String getName() {
    return name;
  }


  public void setName(String name) {
    this.name = name;
  }

}

public class Groovy2 {

  public static void main(String[] args) {
    ScriptEngineManager manager = new ScriptEngineManager();
    ScriptEngine engine = manager.getEngineByName("groovy");
    try {

      Person p = new Person("Ala");
      String call = engine.getFactory().getMethodCallSyntax("p", "setName", "\"Pies\"");

      System.out.println(call);
      engine.put("p", p);

      engine.eval(call);
      System.out.println(p.getName());

      String out = engine.getFactory().getOutputStatement(p.getName());
      System.out.println(out);
      engine.eval(out);

    } catch (ScriptException e) {
      e.printStackTrace();
    }

  }

}

Wynik:

p.setName("Pies")
Pies
println("Pies")
Pies



4.6. Przykład: programowanie skryptowe na zmiennych aplikacji


W poniższym programie przedstawiono:
Wykonywany skrypt wygląda następująco:
importPackage(javax.swing);
a = a + 1;
b = 33;
c = a + b;
sum = 0;
for (i in arr) { 
  arr[i] = arr[i]*2;
  sum += arr[i];
}
JOptionPane.showMessageDialog(null, "Suma = " + sum);
infoText.setText("Suma = " + sum);
przy czym zmienne a, b, c są zmiennymi liczbowymi z aplikacji, arr - tablica z aplikacji, infoText - pole tekstowe z aplikacji.

Aplikacja ma następującą postać:
class Varman {
  
  private Object vars;
  private Bindings bindings;
  private Field[] fields;
  
  public Varman(Bindings bindings, Object vars) {
    this.vars = vars;
    this.bindings = bindings;
    fields = vars.getClass().getDeclaredFields();
    for (int i = 0; i < fields.length; i++) {
      try {
        String name = fields[i].getName();
        if (name.indexOf("this$") != -1) continue;
        Object value =  fields[i].get(vars);
        bindings.put(name, value);
      } catch (Exception e) {
        e.printStackTrace();
      } 
    }
  }
  
  public void getVars() {
    for (int i = 0; i < fields.length; i++) {
      String name = fields[i].getName();
      if (name.indexOf("this$") != -1) continue;      
      Object value =  bindings.get(name);
      try {
        fields[i].set(vars, value);
      } catch (Exception e) {
        e.printStackTrace();
      } 
    }
  }
  
  public String getVarsPageView() {
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < fields.length; i++) {
      try {
        String name = fields[i].getName();
        if (name.indexOf("this$") != -1) continue;
        Object value = fields[i].get(vars);
        if (fields[i].getType().isArray()) {
          StringBuilder tmp = new StringBuilder("{ ");
          tmp.append(Array.get(value, 0));
          for (int j=1; j < Array.getLength(value); j++) tmp.append(", ").append(Array.get(value, j));
          tmp.append(" }");
          value = tmp;
        }
        sb.append('\n').append(name).append(" = ").append(value);
      } catch (Exception e) {
        e.printStackTrace();
      } 
    }
    return sb.toString();
  }
}


public class CalcJs1 {

  private ScriptEngineManager manager = new ScriptEngineManager();
  private ScriptEngine engine = manager.getEngineByName("rhino");
  private Bindings bindings = engine.createBindings();
  
  private class Var {
    double a = 7, b, c, d, e, f;
    int[] arr = { 1, 2, 3 };
  }
  
  public CalcJs1() {
    Var v = new Var();
    Varman vm = new Varman(bindings, v);
    showInfoFrame(bindings);
    try {
      engine.eval(new FileReader("testCalc.js"), bindings);
    } catch (FileNotFoundException e) {
      e.printStackTrace();
    } catch (ScriptException e) {
      e.printStackTrace();
    }
    vm.getVars();
    JOptionPane.showMessageDialog(null, vm.getVarsPageView(), "Wyniki", 1);
    for (int i = 0; i < v.arr.length; i++) {
      System.out.println("arr " +i+ " = " + v.arr[i]);
    }
  }  
  
  private void showInfoFrame(Bindings bindings) {
    JFrame f = new JFrame("Wyniki");
    JTextField tf = new JTextField("Na razie nie ma wyniku               ");
    f.add(tf);
    f.pack();
    f.setLocation(600, 300);
    f.setVisible(true);
    bindings.put("infoText", tf);
  }

  public static void main(String[] args) {
    new CalcJs1();
  }
  

}


Pokaz działania aplikacji.r


4.7. Przykład: wykorzystanie możliwości JavaScript w apletach Javy


W tym przykładzie stworzymy aplet, dostarczający "eleganckiego" menu.
Jedna z opcji menu daje możliwość zmiany stylu strony.
Aby uzyskać zmianę stylu wykorzystamy motor skryptowy dla natywnych przeglądarek (BrowserJS) i napiszemy odpowiedni skrypt js, który będzie wykonywany po wyborze opcji w menu apletu.

Demonstracja działaniar

Skrypt:

function chgStyle(cssFile) {
  var link = document.getElementById("slink");
  link.setAttribute('href', cssFile);
}
Aplet:
import java.awt.*;
import java.awt.event.*;
import java.io.*;

import javax.script.*;
import javax.swing.*;

@SuppressWarnings("serial")
public class Konfigurator extends JApplet implements ActionListener {

  ScriptEngine engine;
  String[] cssFiles = { "style/first.css", "style/second.css", "style/third.css", "style/last.css" };
  String[] opis = { "Surowy", "Kolorowy", "Powiększony", "Słoneczny" };
  String script;

  @Override
  public void init() {
    ClassLoader myloader = getClass().getClassLoader();
    ScriptEngineManager manager = new ScriptEngineManager(myloader);
    engine = manager.getEngineByName("BrowserJS");
    engine.put("applet", this);
    
    BufferedReader br;
    StringBuilder sb = new StringBuilder();
    try {
      br = new BufferedReader(new FileReader("menu.js"));
      for (String line; (line = br.readLine()) != null;)
        sb.append(line).append('\n');
    } catch (IOException e1) {
      e1.printStackTrace();
    }
    
    script = sb.toString();
    
    JMenu[] menu = { new JMenu("Wygląd"), new JMenu("Spis treści"), 
                     new JMenu("Ważne linki"), new JMenu("Wyszukiwanie"),  new JMenu("Pomoc") }; 
    for (int i = 0; i < opis.length; i++) {
      JMenuItem mi = new JMenuItem(opis[i]);
      mi.setActionCommand(cssFiles[i]);
      mi.addActionListener(this);
      menu[0].add(mi);
    }
     
    JMenuBar mb = new JMenuBar();
    for (int i = 0; i < menu.length; i++) {
      if (i == menu.length-1) mb.add(Box.createHorizontalGlue());
      mb.add(menu[i]);
      menu[i].addMouseListener(new MouseAdapter() {
        Color back; 
        @Override
        public void mouseEntered(MouseEvent e) {
          Component c = e.getComponent();
          back = c.getForeground();
          c.setForeground(Color.BLUE);
        }

        @Override
        public void mouseExited(MouseEvent e) {
          e.getComponent().setForeground(back);
        }
        
      });
    }

    this.setJMenuBar(mb);
  }
  
   public void actionPerformed(ActionEvent e) {
        try {
          String name = e.getActionCommand();
          engine.eval(script);
          if (engine instanceof Invocable) {
            Invocable inv = (Invocable) engine;
            inv.invokeFunction("chgStyle", name);
          }
          else JOptionPane.showMessageDialog(null, "Engine not invocable");
        } catch (Exception exp) {
          throw new RuntimeException(exp);
        }
  }
   
}



5. Zadania i ćwiczenia


Będą podane w trakcie semestru