© Copyright Azzurri Ltd. 2003, 2004. All rights reserved.
 Eclipse-artikel

JET-øveprogram del 2 (Skriv kode, der skriver kode)

Oversigt

I del 2 af dette øveprogram til JET (Java Emitter Templates) ser vi nærmere på JET-programmets API. Vi vil gennemgå, hvordan du skriver plugins, der bruger klasserne i JET-pakken til at generere Java-kildekode.

Som et praktisk eksempel vil vi oprette en plugin, der ud fra brugerinput genererer en typesikker enumerationsklasse. Den genererede kildekode er baseret på en JET-skabelon, der kan distribueres sammen med plugin'en, så brugerne af denne plugin kan tilpasse den genererede kode ved at redigere skabelonen.

Artiklen indeholder også en kort oversigt over JET API'et.

Bidragyder: Remko Popma, Azzurri Ltd., remko.popma snabel-a azzurri punktum jp, 26. august 2003. Bruges med tilladelse. Sidst opdateret: 3. januar 2007.


Indhold

Introduktion
Nogle JET-klasser
En plugin, der genererer kildekode
Konklusion
Appendiks
Ressourcer

Introduktion

Omdannelse i forhold til generering

Et aspekt ved JET-skabeloner kan virke forvirrende til at begynde med. Det er, at generering består af to trin: omdannelse og generering. Det første trin består i at omdanne skabelonen til en skabelonimplementeringsklasse. Det andet trin består i at bruge denne skabelonimplementeringsklasse til at generere teksten.

Hvis dit mål med JET er at generere Java-kode, kan det være forvirrende, at resultatet af skabelonomdannelsestrinnet også er Java-kildekode. Husk, at denne kildekode ikke er den genererede tekst. Den kildekode, der er resultatet af omdannelsestrinnet, er blot en anden udgave af skabelonen.

Hvis du har brugt JSP og miniservere (servlets) før, kan du betragte en JET-skabelon som svarende til en JSP-side. En JET-skabelon omdannes til en skabelonimplementeringsklasse, ligesom en JSP-side omdannes til en miniserver. Det andet trin, hvor skabelonimplementeringsklassen genererer tekst, svarer til, at miniserveren opretter og returnerer HTML.

Del 1 af dette øveprogram introducerede JET-skabeloner og beskrev, hvordan du kan konvertere et projekt til et JET-projekt for at få JET-byggeprogrammet til automatisk at omdanne skabeloner i dit projekt til skabelonimplementeringsklasser.

I del 2 af øveprogrammet vil vi fokusere på at skrive en plugin, der bruger klasserne i JET-pakken til at generere Java-kildekode. En plugin, der genererer tekst fra en JET-skabelon, kan ikke få omdannet skabeloner automatisk ved hjælp af JET-naturen og JET-byggeprogrammet. Det skyldes, at JET-naturen og JET-byggeprogrammet kun arbejder med projekter i arbejdsområdet, ikke med plugins. Plugins er nødt til at bruge klasserne i JET-pakken til at omdanne deres egne skabeloner.

Du kan pakke org.eclipse.emf.examples.jet.article2_2.3.0.zip ud i dit plugins-underbibliotek, hvis du vil udføre eksemplet eller se kildekoden, der bruges i denne artikel. Du skal have EMF installeret for at bruge plugin-eksemplet. Artiklens forfatter har brugt version 2.3.0 M4.

Næste afsnit omhandler nogle af klasserne i pakken org.eclipse.emf.codegen. Vi vil gennemgå de trin, der kræves for at generere kildekode med JET, og hvordan JET-programklasserne passer ind. Hvis du er utålmodig efter at se noget kode, der viser, hvordan klasserne bruges i praksis, kan du gå direkte til En plugin, der genererer kildekode.

Nogle JET-klasser

I dette afsnit ser vi nærmere på nogle af klasserne i JET-pakken. De kan stort set inddeles i to grupper:

Klasserne på lavt niveau beskrives ikke nærmere i denne artikel. En beskrivelse af alle klasserne i plugin'en org.eclipse.emf.codegen finder du i afsnittet Oversigt over JET API nedenfor. I resten af dette afsnit fokuserer vi på et par af klasserne på højt niveau.

org.eclipse.emf.codegen.jet.JETCompiler

JETCompiler er kerneklassen til omdannelse af skabeloner. Klassen har ansvaret for at omdanne skabeloner til Java-kildekoden til en skabelonimplementeringsklasse. Den egentlige omdannelse delegeres til andre klasser i samme pakke. Klienter opretter et JETCompiler-objekt til en bestemt skabelon og kalder derefter metoden parse efterfulgt af metoden generate for at skrive Java-kildekoden til den resulterende skabelonimplementeringsklasse til en angivet strøm.

org.eclipse.emf.codegen.jet.JETEmitter

JETEmitter er et praktisk API på højt niveau for brugere af JET-pakken. Metoden generate i denne klasse kombinerer skabelonomdannelse og tekstgenerering i ét trin. JETEmitter tager sig af alle detaljerne i forbindelse med omdannelse af skabeloner og kompilering af Java-kildekoden til den omdannede skabelonimplementeringsklasse, så du kan fokusere på det endelige output fra generatoren.

Man kan også opfatte JETEmitter som en afskærmning omkring omdannelsestrinnet, så det virker, som om du kan generere tekst direkte ud fra en skabelon. Ifølge loven om lækkende abstraktioner (Law of Leaky Abstractions) kan vi ikke altid slippe afsted med det, og i afsnittet JETEmitter-faldgruber nedenfor nævnes et par steder, hvor du skal være forsigtig.

JETEmitter er den klasse, vi vil bruge i vores plugin, så vi går lidt mere i dybden her.

Et JETEmitter-objekt konstrueres med URI'en for den skabelon, der bruges til at generere tekst. Alle URI-typer kan angives, blot en protokolbehandler er til rådighed. Det betyder, at både URI'er med typen file:/, ftp:/ og http:/ kan bruges. Eclipse tilføjer særlige protokolbehandlere til URI'erne platform:/base/, platform:/plugin/, platform:/fragment/ og platform:/resource/, så plugins kan bruge en URI som platform:/resource/myproject/myfolder/mytemplate.jet til at angive en skabelonfil. Bemærk: I Eclipse 3.0 er bundleentry tilføjet til listen med særlige protokoller. Den bør bruges i referencer til Eclipse-elementer som f.eks. plugins og funktioner.

I vores plugin-eksempel vil vi distribuere vores skabelonfil sammen med plugin'en, så skabelonfilen vil blive placeret i folderen myplugin/templates under Eclipse-folderen plugins. Derefter kan følgende kode bruges til at finde og generere en skabelon fra denne folder:

 String pluginId = "myplugin.id";
 String base = Platform.getBundle(pluginId).getEntry("/").toString();
 String uri = base + "templates/myTemplate.javajet";
 JETEmitter emitter = new JETEmitter(uri);
 String generatedText = emitter.generate(new Object[] {parameter});

Når der er konstrueret et JETEmitter-objekt, kalder klienter generate for det for at generere tekst. Metoden generate udfører følgende trin:

  1. Opretter et projekt ved navn .JETEmitters i arbejdsområdet
  2. Klargør projektet ved at tildele det Java-naturen og tilføje klassestivariabler til dets classpath
  3. Omdanner skabelonen til en Java-kildefil til skabelonimplementering i projektet .JETEmitters
  4. Bygger projektet for at kompilere kildekoden til skabelonimplementering som en Java .class-fil
  5. Kalder metoden generate på den omdannede Java-skabelonimplementeringsklasse, og returnér den genererede tekst som en streng

* .JETEmitters er standardnavnet på det projekt, der oprettes under skabelonomdannelsen. Værdien kan ændres med metoden setProjectName.

Vores plugin-eksempel vil bruge JETEmitter og gemme den genererede tekst som en Java-kildefil i arbejdsområdet. I figuren nedenfor vises de trin, som generering af kildekode ved hjælp af JETEmitter består af.

Generér tekst fra en plugin ved hjælp af JETEmitter

JETEmitter-faldgruber

Klassen JETEmitter kombinerer skabelonomdannelse og tekstgenerering i ét trin, hvilket gør den til et meget praktisk værktøj. Det er imidlertid vigtigt at vide, hvad der foregår bag kulisserne. Ellers kan du få nogle ubehagelige overraskelser. I dette afsnit beskrives nogle almindelige "faldgruber", så du ikke begår samme fejl.

1. Plugin skal initialiseres

Det er ikke let at bruge JET uden for Eclipse. JET er kun designet til at blive udført som et arbejdsområdeprogram. Ethvert program, der bruger JET, skal som minimum udføres som et "hovedløst" Eclipse-program, så der foretages initialisering af plugin'en. (Betegnelsen hovedløs dækker over udførelse af Eclipse uden brugergrænsefladen).

Det betyder, at det ikke virker, hvis du prøver at bruge JETEmitter fra et simpelt enkeltstående program (en Java-standardklasse med en main-metode):

 // This fails: cannot use JETEmitter from a standalone application
 public static void main(String[] args) {
     JETEmitter emitter = new JETEmitter("/myproject/templates/HelloWorld.txtjet");
 
     // this will throw a NullPointerException
     String result = emitter.generate(new NullProgressMonitor(), {"hi" });
 
     System.out.println(result);

Bemærk, at det ikke kun er en begrænsning for klassen JETEmitter. Mange af klasserne i plugin'en org.eclipse.emf.codegen har afhængigheder af andre plugins. I afsnittet Appendiks nedenfor er der flere oplysninger om brug af JET fra enkeltstående programmer.

I resten af denne artikel forudsætter vi, at koden udføres fra en plugin.

2. Forhold i forbindelse med klasseindlæsning

Du kan få en fejl af typen Ingen klassedefinition er fundet, hvis du overfører et tilpasset objekt som argument til metoden JETEmitter.generate. Det kan ske, hvis det objekt, du overfører som argument, ikke tilhører en af "bootstrap"-klasserne i Java ("bootstrap"-klasserne er runtime-klasserne i rt.jar og internationaliseringsklasserne i i18n.jar).

Du kan undgå fejlen ved at angive klasseindlæseren til din plugin, når du bruger JETEmitter. Hvis der ikke angives en klasseindlæser, bruger JETEmitter sin egen klasses klasseindlæser. Det er normalt klasseindlæseren til plugin'en org.eclipse.emf.codegen, og denne klasseindlæser kan ikke se ret meget. I nyere versioner af EMF (fra version 1.1.0 build 20030527_0913VL) har JETEmitter en konstruktør med en klasseindlæser som argument.

Bemærk, at en anden måde at angive en klasseindlæser på er at oprette en underklasse af JETEmitter i dit eget projekt. Hvis der ikke angives en klasseindlæser, vil JETEmitter bruge denne underklasses klasseindlæser. (Hvis du bruger en ældre version af EMF, er der ingen konstruktører, der kan modtage et klasseindlæserargument, og du har ikke andet valg end at oprette en underklasse af JETEmitter i dit eget projekt).

I eksemplet herunder vises en funktionsklasse, der omdanner og kalder en valgt skabelon ved hjælp af JETEmitter. Eksemplet viser, hvordan JETEmitter kan konstrueres med en klasseindlæserparameter eller ved at konstruere en anonym underklasse.

package org.eclipse.emf.examples.jet.article2.actionexample;
// imports omitted
public class EmitAction implements IActionDelegate {
    protected ISelection selection;
 
    public void selectionChanged(IAction action, ISelection selection) {
        this.selection = selection;
        action.setEnabled(true);
    }
 
    public void run(IAction action) {
        List<?> files = (selection instanceof IStructuredSelection)
                ? ((IStructuredSelection) selection).toList()
                : Collections.EMPTY_LIST;
                
        for (Iterator<?> i = files.iterator(); i.hasNext();) {
            IFile file = (IFile) i.next();
            IPath fullPath = file.getFullPath();
 
            String templateURI = "platform:/resource" + fullPath;
            ClassLoader classloader = getClass().getClassLoader();
         JETEmitter emitter = new JETEmitter(templateURI, classloader);
 
            // or: use an anonymous subclass

         // emitter = new JETEmitter(templateURI) {}; // notice the brackets
            
            try {
                IProgressMonitor monitor = new NullProgressMonitor();
                String[] arguments = new String[] { "hi" };
 
                String result = emitter.generate(monitor, arguments);
                
                saveGenerated(result, file);
 
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
    }
 
    // saveGenerated method omitted
}

3. Forhold i forbindelse med classpath

JETEmitter omdanner dine skabeloner til Java-kildefiler i projektet .JETEmitters og kalder JavaBuilder for at få kompileret disse kildefiler. Hvis dine skabeloner bruger klasser, der ikke er Java-standardklasser eller ikke findes i EMF-plugin'en, må du tilføje disse klasser til .JETEmitters-projektets classpath. Ellers kan JavaBuilder ikke kompilere kildefilerne til skabelonimplementering. Heldigvis gør JETEmitter det let via metoden addVariable, som tilføjer en classpath-variabel til .JETEmitters-projektet.

En classpath-variabel er et navn, der refererer til en JAR-fil eller et bibliotek, og som gælder i hele arbejdsområdet. Listen med alle variablerne af denne type kan ses med menupunktet Vindue > Indstillinger > Java > Classpath-variabler. Dit program skal tilføje en classpath-variabel for hver JAR-fil eller hvert bibliotek, der skal findes i classpath til .JETEmitters-projektet.

En plugin, der genererer kildekode

I denne del af øveprogrammet til JET vil vi skrive en plugin, der bruger en JET-skabelon til at generere Java-kildekode til typesikre enumerationer, som var ret populære før Java 5.0, hvor enums blev indført i sproget.

Vores plugin skal udføre følgende opgaver:

  1. Indsamle brugerinputværdier for variablerne i skabelonen: klassenavnet, typen og navnet på den typesikre enumerationsklasses attributter og værdien af disse attributter for hver forekomst. Vi vil skrive en enkel brugergrænseflade for at indsamle disse værdier.
  2. Omdanne JET-skabelonfilen til en Java-skabelonimplementeringsklasse
  3. Kalde skabelonimplementeringsklassen med et objekt, som indeholder de brugerinputværdier, der er indsamlet af brugergrænsefladen
  4. Gemme den genererede resultatkildekode på en placering, der er hentet fra brugergrænsefladen

I de næste afsnit gennemgår vi ovennævnte trin et for et.

Typesikre enumerationer

Lad os se på en typesikker enumerationsklasse for at undersøge, hvilken slags kildekode vi vil generere. Klassen Digit herunder er et eksempel på en typesikker enum.

 // an example typesafe enum
 package x.y.z;
 public class Digit { 
  public static final Digit ZERO = new Digit(0, "zero");
     public static final Digit ONE = new Digit(1, "one");
     public static final Digit TWO = new Digit(2, "two");
     public static final Digit THREE = new Digit(3, "three");
     // ...
     public static final Digit NINE = new Digit(9, "nine");
 
     private static final Digit[] ALL = 
         {ZERO, ONE, TWO, THREE, FOUR, FIVE, SIX, SEVEN, EIGHT, NINE};
 

  private final int value;
     private final String name;
 
     private Digit(int value, String name) { 
         this.value = value; 
         this.name = name; 
     }
 
  public static Digit lookup(int key) {
         for (int i = 0; i < ALL.length; i++) {
             if (key == ALL[i].getValue()) { return ALL[i]; }
         }
         // lookup failed:
         // we have no default Digit, so we throw an exception
      throw new IllegalArgumentException("No digit exists for " + key);
     }
 
     public int getValue() { return value; }
     public int getName() { return name; }
     public String toString() { return getName(); }
 }

Lad os undersøge klassen nærmere. For det første har klassen Digit flere forekomster - konstanterne ZERO, ONE, TWO osv. Hver forekomst defineres af dens Java-variabelnavn, "ZERO", "ONE", "TWO"..., og værdien af hver attribut for enumerationsklassen. De fleste typesikre enums har en eller flere attributter. Klassen Digit har to attributter: value, der er et heltal, og name, som er en streng.

Vores Digit-eksempelklasse har også en lookup-metode, som returnerer den forekomst, hvis value-attribut er lig med den angivne int-parameter. Opslagsmetoden lookup introducerer begrebet nøgleattributter. Mange typesikre enums har en eller flere attributter, så der entydigt kan skelnes mellem forekomsterne.

Bemærk, at nøgleattributter ikke er et krav. Java VM garanterer, at ethvert netop oprettet objekt er entydigt, og det er derfor muligt at have typesikre enumerationer uden nogen attributter overhovedet og blot skelne mellem forekomsterne med identitetsoperatoren ==. Det fungerer fint, men det er ofte praktisk at have en nøgleattribut, der identificerer en forekomst entydigt, og en lookup-metode, som finder forekomsten for en angivet nøgleværdi.

Vores skabelon har en lookup-metode, så vi skal beslutte, hvad der skal gøres, hvis der ikke bliver fundet en forekomst for den angivne nøgleværdi. Der er grundlæggende tre muligheder: afsend en undtagelse, returnér en fastlagt "standardforekomst", eller returnér NULL. Hvilken mulighed der er den bedste, afhænger af det program, som klassen bruges i, så vi skal nok lade det være op til brugeren.

Vi har nu undersøgt typesikre enums nærmere og kan sammenfatte det, der kan tilpasses i en typesikker enumeration:

En simpel model med en typesikker enumeration

En simpel model for de dele af en typesikker enum, der kan tilpasses, kan se sådan ud:

TypesafeEnum
getInstances() : Instance[]
getAttributes() : Attribute[]
getKeyAttributes() : Attribute[]
getDefaultInstance() : Instance
getPackageName() : String
getClassName() : String

Forekomst
getName() : String
getAttributeValues() : Properties
getAttributeValue(Attribute) : String
isDefault() : boolean

Attribut
getName() : String
getType() : String
isKey() : boolean

I næste afsnit vil vi bruge disse klasser til at konvertere vores Digit-klasse til en JET-skabelon til typesikre enumerationer.

Skabelon til typesikker enumeration

Nu hvor vi har en model, kan vi tage vores Digit-klasse og erstatte al Digit-specifik kode med JET-minikommandofiler og udtryk, som kalder vores modelklasser. Resultatskabelonen kan se sådan ud:

 <%@ jet package="translated" imports="java.util.* org.eclipse.emf.examples.jet.article2.model.*" class="TypeSafeEnumeration" %>
 <% TypesafeEnum typesafeEnum = (TypesafeEnum) argument; %>
 package <%=typesafeEnum.getPackageName()%>;
 
 /**
  * This final class implements a type-safe enumeration
  * over the valid instances of a <%=typesafeEnum.getClassName()%>.
  * Instances of this class are immutable.
  */
 public final class <%=typesafeEnum.getClassName()%> {
 
 <% for (Iterator<Instance> i = typesafeEnum.instances(); i.hasNext(); ) { %>

 <%     Instance instance = i.next(); %>
 
     // instance definition
     public static final <%=typesafeEnum.getClassName()%> <%=instance.getName()%> = 
      new <%=typesafeEnum.getClassName()%>(<%=instance.constructorValues()%>);
 <% } %>

 
 <% for (Iterator<Attribute> i = typesafeEnum.attributes(); i.hasNext(); ) { %>
 <%     Attribute attribute = i.next(); %>
 
     // attribute declaration
  private final <%=attribute.getType()%> m<%=attribute.getCappedName()%>;
 <% } %>

 
     /**
      * Private constructor.
      */
  private <%=typesafeEnum.getClassName()%>(<%=typesafeEnum.constructorParameterDescription()%>) {
 <% for (Iterator<Attribute> i = typesafeEnum.attributes(); i.hasNext(); ) { %>
 <%     Attribute attribute = i.next(); %>
      m<%=attribute.getCappedName()%> = <%=attribute.getUncappedName()%>;
 <% } %>

     }
               
 // getter accessor methods
 <% for (Iterator<Attribute> i = typesafeEnum.attributes(); i.hasNext(); ) { %>
 <%     Attribute attribute = i.next(); %>
     /**
      * Returns the <%=attribute.getName()%>.
      *
      * @return the <%=attribute.getName()%>. 
      */
     public <%=attribute.getType()%> get<%=attribute.getCappedName()%>() {
         return m<%=attribute.getCappedName()%>;
     }
 
 <% } %>     
 
     // lookup method omitted...
 }

Som det ses, kalder skabelonen nogle metoder, der ikke fandtes i den simple model, vi introducerede tidligere. Vi har tilføjet et par bekvemmelighedsmetoder, f.eks. metoderne Attribute.getCappedName() og getUncappedName(). Den type metoder hjælper med at holde skabelonen enkel.

Vi har også tilføjet andre metoder til modellen, herunder metoderne TypesafeEnum.constructorParameterDescription() og Instance.constructorValues(). Implementeringen af metoden constructorValues er vist nedenfor.

// class Instance
/**
 * Convenience method that returns the attribute values of this instance,
 * in the order expected by the constructor of this instance.
 * 
 * @return a comma-separated list of all attribute values of this instance,
 *         formatted like attrib1-value, attrib2-value (, ...)
 */
public String constructorValues() {
    StringBuffer result = new StringBuffer();
    for (Iterator<Attribute> i = getType().attributes(); i.hasNext(); ) {
        Attribute attribute = i.next();
        result.append(getAttributeValue(attribute));
        if (i.hasNext()) {
            result.append(", ");
        }
    }
    return result.toString();
}

Metoden constructorValues gennemløber den typesikre enums attributter, slår hver attributs værdi op i forekomsten og sammenkæder disse værdier adskilt med komma i en streng. I vores typesikre Digit-enum-klasse ovenfor ville metoden returnere "0, \"zero\"" for forekomsten "ZERO".

Vi kunne have gennemløbet attributværdierne i skabelonen, men det ville have gjort skabelonen meget sværere at læse. Når denne logik skubbes ind i modellen, bliver skabelonen mere læsevenlig og lettere at vedligeholde. På den anden side har vi mistet fleksibilitet, fordi brugeren ikke længere kan tilpasse denne logik ved at redigere skabelonen. Det er en afvejning, man må foretage. Det bedste valg afhænger af din skabelon og dit program.

Brugergrænseflade til indsamling af brugerinput

Vi har nu en model og en skabelon og mangler to dele mere, før vores plugin er færdig. Vi skal have en brugergrænseflade, der indsamler værdier fra brugeren, som vores model skal udfyldes med, og vi skal kalde vores skabelon med den udfyldte model for at generere kildekode og gemme denne kildekode et sted i arbejdsområdet.

Vi begynder med brugergrænsefladen. Arbejdsbænken stiller et par guider til rådighed, der gør noget i den stil, vi har tænkt os, f.eks. guiderne Ny klasse, Ny grænseflade og Ny JUnit-testcase. Det giver formentlig mening at få vores brugergrænseflade til at ligne disse guider og give adgang til den fra standardplaceringerne på menu og værktøjslinje.

Vores guide har tre sider. Den første side, der er vist nedenfor, ligner en forenklet udgave af guiden Ny klasse. Faktisk bruger vi samme struktur, som guiden Ny klasse bruger, nemlig pakken org.eclipse.jdt.ui.wizards. På den første side indsamler vi pakkenavnet og klassenavnet for den typesikre enum og det sted, hvor resultatet skal gemmes.

Side 1 i brugergrænsefladeguide: klasse, pakke og placering for typesikker enum

Side 2 indsamler oplysninger om den typesikre enum-klasses attributter. Hver attribut har et navn og en type og kan være en af nøgleattributterne. Side 2 i guiden er vist nedenfor:

Side 2 i brugergrænsefladeguide: den typesikre enums attributter

Den tredje og sidste side i guiden, som vises herunder, indsamler oplysninger om den typesikre enums forekomster. Brugeren indtaster forekomstnavnet og angiver værdier for alle attributterne for hver forekomst.

En af forekomsterne kan være standardforekomsten "Default", som er den forekomst, der returneres af lookup-metoden, hvis der ikke bliver fundet en forekomst for de angivne nøgleattributværdier.

Side 3 i brugergrænsefladeguide: den typesikre enums forekomster

Start skabelonen

Vi har nu en brugergrænseflade, der kan udfylde vores model, og kan endelig bruge det, vi gennemgik i den første del af artiklen, og generere kildekode med vores skabelon.

Når en bruger klikker på Afslut i guide, kaldes metoden performFinish i vores guide. Koden herunder viser, hvordan vi bruger en tilpasset underklasse af JETEmitter til at tilføje JAR-filen til vores plugin til .JETEmitters-projektets classpath, før vi kalder generate på JETEmitter. Den genererede kildekode til den typesikre enum gemmes på den placering i arbejdsområdet, som brugeren angav.

 // class NewTypesafeEnumCreationWizard
 protected void finishPage(IProgressMonitor monitor) 
 throws InterruptedException, CoreException {
 
     String pluginId = "org.eclipse.emf.examples.jet.article2";
     String base = Platform.getBundle(pluginId).getEntry("/").toString();
     String relativeUri = "templates/TypeSafeEnumeration.javajet";
  JETEmitter emitter = new JETEmitter(base + relativeUri, getClass().getClassLoader());
  emitter.addClasspathVariable("JET_TUTORIAL", pluginId);
 
     TypesafeEnum model = mPage1.getTypesafeEnumModel();
     IProgressMonitor sub = new SubProgressMonitor(monitor, 1);

  String result = emitter.generate(sub, new Object[] { model });
     monitor.worked(1);
 
  IFile file = save(monitor, result.getBytes());
 
     selectAndReveal(file);
     openResource(file);
 }

Registrér guiden

Det sidste kodestykke nedenfor viser den del af vores plugin.xml-konfigurationsfil, hvor vi registrerer vores guide som et bidrag til arbejdsbænken.

   <extension point="org.eclipse.ui.newWizards">
      <wizard 
            name="Typesafe Enum"
            icon="icons/newenum_wiz.gif"
            category="org.eclipse.jdt.ui.java"
            id="org.eclipse.emf.examples.jet.article2.ui.NewTypesafeEnumCreationWizard">
         <description>
            Create a Typesafe Enumeration
         </description>
         <class class="org.eclipse.emf.examples.jet.article2.ui.NewTypesafeEnumCreationWizard">
         <parameter name="javatype" value="true"/>
         </class>
      </wizard>
   </extension>

En bruger kan nu aktivere guiden ved at vælge Fil > Ny > Andet > Java > Typesafe Enum fra arbejdsbænken som vist på billedet herunder.

Guiden Typesafe Enum vises blandt andre guider til oprettelse af en ny forekomst

Bemærk, at vi har angivet attributten javatype til "true" i udvidelseselementet wizard i filen plugin.xml. Det bevirker, at vores guide vises som en mulighed på værktøjslinjen i perspektivet Java som illustreret på billedet herunder.

Guiden Typesafe Enum vises som en mulighed på værktøjslinjen i perspektivet Java

Konklusion

JET kan være til stor nytte for programmer, der skal generere tekst. Skabeloner er en lige så stor forbedring for kodegenerering, som JSP-sider var i forhold til tidligere tiders miniservere.

Når du bruger JET, skal du beslutte, om du vil distribuere dine skabeloner sammen med programmet eller kun distribuere skabelonimplementeringsklasserne.

Hvis dit mål er at forenkle dit programs tekstgenereringsfunktioner, er det et godt valg at bruge JET-naturen og JET-byggeprogrammet til at omdanne dine skabeloner automatisk. Der er flere oplysninger i JET-øveprogram del 1. I det tilfælde behøver du blot at distribuere de omdannede skabelonimplementeringsklasser sammen med programmet, ikke selve skabelonerne.

Hvis det derimod er vigtigt for programmet, at brugerne har fuld kontrol over den genererede tekst, vil du måske distribuere selve skabelonfilerne sammen med programmet. I så fald skal disse skabeloner omdannes, hver gang der genereres tekst. Den plugin, vi har skrevet i denne artikel, er et eksempel på denne type program.

Denne artikel har beskrevet, hvilke klasser der er til rådighed i JET-pakken til dette formål, og vist, hvordan disse klasser bruges sammen med en Eclipse-plugin. Appendikset herunder indeholder en oversigt over JET API'et og viser, hvordan det kan bruges i hovedløse og enkeltstående programmer.

Appendiks

Oversigt over JET API

Pakken org.eclipse.emf.codegen

Klasse Beskrivelse
CodeGen

Klassen CodeGen kan omdanne en JET-skabelon til Java-kildekode og eventuelt flette Java-kildekoden til skabelonimplementering sammen med en eksisterende Java-klasse. CodeGen kan bruges som et hovedløst Eclipse-program. Metoden run forventer en parameter med typen strengarray bestående af to eller tre elementer:

  • URI'en for den skabelon, der skal omdannes
  • den destination, hvor resultatet af omdannelsen skal gemmes
  • (valgfrit) en JMerge-kontrolmodelfil, der angiver, hvordan det nye omdannelsesresultat skal flettes med kildekoden til en eksisterende Java-klasse
CodeGenPlugin Plugin-klassen til JET-pakken.

Pakken org.eclipse.emf.codegen.jet

Klasse Beskrivelse
IJETNature Grænseflade, der udvider org.eclipse.core.resources.IProjectNature. Definerer nogle af egenskaberne for en JET-natur. Implementeres af JETNature. Bruges som filter for projektegenskabssiderne af plugin'en org.eclipse.emf.codegen.ui.
JETAddNatureOperation En org.eclipse.core.resources.IWorkspaceRunnable-forekomst til tilføjelse af JET-naturen til et projekt i arbejdsområdet. Bruges af AddJETNatureAction i plugin'en org.eclipse.emf.codegen.ui.
JETBuilder Denne klasse udvider org.eclipse.core.resources.IncrementalProjectBuilder. Når dens build-metode kaldes, delegerer den til JETCompileTemplateOperation for at få omdannet alle skabeloner i arbejdsområdeprojektet, der er ændret siden sidste bygning. Skabelonerne skal være placeret i en af de foldere, der er angivet som Skabelonopbevaring i projektets JET-natur.
JETCharDataGenerator Er ansvarlig for en del af processen med omdannelse af skabeloner. Genererer strenge for de tegndata, der findes i skabelonfilen. Bruges af JETCompiler.
JETCompiler Dette er kerneklassen til omdannelse af skabeloner. Klassen har ansvaret for at omdanne skabeloner til Java-kildekoden til en skabelonimplementeringsklasse. Den egentlige omdannelse delegeres til andre klasser i denne pakke. JETParser bruges til at analysere skabelonen og opdele den i skabelonelementer. JETCompiler implementerer grænsefladen JETParseEventListener og registrerer sig i parseren for at få besked, når parseren genkender et skabelonelement. For hvert genkendt skabelonelement bruger JETCompiler en JETGenerator-forekomst til at omdanne skabelonelementet til Java-kildekode. Når skabelonanalysen er færdig, bruger JETCompiler en forekomst af JETSkeleton til at foretage assemblering af Java-kildekodeelmenterne, så de udgør en enkelt kompileringsenhed (en Java-klasse).
JETCompileTemplateOperation Denne klasse implementerer org.eclipse.core.resources.IWorkspaceRunnable, så den kan udføres som en batchfunktion i arbejdsområdet. Funktionen skal have et arbejdsområdeprojekt, et eller flere skabelonopbevaringssteder og eventuelt en liste med bestemte skabelonfiler som konstruktørparametre. Når dens run-metode kaldes, bruger den JETCompiler til at omdanne skabelonfilerne i arbejdsområdeprojektets angivne foldere til Java-kildefiler til skabelonimplementeringsklasser. Funktionen kan valgfrit konfigureres, så den udløser en fuldstændig bygning af projektet, når den er færdig, for at kompilere Java-kildefilerne til .class-filer.
JETConstantDataGenerator Er ansvarlig for en del af processen med omdannelse af skabeloner. Udvider JETCharDataGenerator for at generere konstanterklæringer for de strenge med tegndata, der findes i skabelonfilen.
JETCoreElement Grænseflade til centrale JET-syntakselementer (direktiv, udtryk, minikommandofil (scriptlet) og escape af anførselstegn). Bruges af JETParser.
JETEmitter Denne klasse er et praktisk API på højt niveau for brugere af denne pakke. Metoden generate i denne klasse omdanner en skabelon til Java-kildekode, kompilerer kildekoden til en skabelonimplementeringsklasse, anmoder skabelonklassen om at generere tekst og returnerer til sidst det genererede resultat. Klassen opretter et Java-projekt ved navn .JETEmitters i arbejdsområdet, omdanner skabelonen ind i dette projekt og kalder build på projektet .JETEmitters for at få kompileret kildekoden. Hvis omdannelsen eller kompileringen mislykkes, afsendes undtagelsen JETException. En Java-klasse til skabelonimplementering "udføres" ved, at dens generate-metode kaldes.
JETException Udvider org.eclipse.core.runtime.CoreException, men stiller mere praktiske konstruktører til rådighed.
JETExpressionGenerator Er ansvarlig for en del af processen med omdannelse af skabeloner. Udvider JETScriptletGenerator for at omdanne JET-udtryk (i <%= ... %>) til Java-kildekode.
JETGenerator Grænseflade til generatorer: Klasser, der er i stand til at omdanne en del af en JET-skabelon til et element i Java-kildekoden.
JETMark Et tilstandsobjekt, som bruges af JETParser til at markere punkter i JET-tegninputstrømmen og delegere behandlingen af dele af strømmen til andre objekter.
JETNature

Denne klasse implementerer IJETNature, så den kan konfigurere et arbejdsområdeprojekt med JET-naturen. Når denne natur tilføjes til et projekt, tilføjer den et JET-byggeprogram øverst i byggespecifikationen til projektet. Naturen definerer to egenskaber:

  • Skabelonopbevaring - en liste med foldere i projektet, som indeholder de JET-skabeloner, der skal omdannes.
  • Kildeopbevaringssted - den målfolder, hvor omdannede Java-klasser til skabelonimplementering skal gemmes.

Disse egenskaber bruges af JET-byggeprogrammet under en bygning.

JETParseEventListener Grænseflade til objekter, der er i stand til at behandle dele af en JET-tegninputstrøm.
JETParser Hovedparserklassen. Har flere indre klasser, der genkender centrale JET-syntakselementer (direktiv, udtryk, minikommandofil (scriptlet) og escape af anførselstegn). Når et centralt JET-syntakselement er genkendt, delegeres den egentlige behandling af elementet til JETParseEventListener.
JETReader En inputbuffer til JET-parseren. Leverer metoden stackStream, som andre kan kalde med tegnstrømmen til en inkluderingsfil. Stiller også mange andre bekvemmelighedsmetoder til rådighed for parseren.
JETScriptletGenerator Er ansvarlig for en del af processen med omdannelse af skabeloner. Omdanner JET-minikommandofiler (scriptlets) (i <% ... %>) til Java-kildekode.
JETSkeleton Denne klasse leverer en grænseflade til assemblering af Java-kildekodeelementer i en enkelt Java-kompileringsenhed (en Java-klasse). Der foretages assemblering af Java-kildekodeelementer i henhold til en klasseskeletdefinition. Et skelet kan bruges til at tilføje genbrugskode til en omdannet skabelonimplementeringsklasse. Klassen stiller en tilpasset standardskeletdefinition til skabelonimplementeringsklasser til rådighed, men kan også foretage assemblering af Java-elementer ved hjælp af et tilpasset skelet. Den egentlige analyse og generering af Java-kildekode delegeres til klasser i pakken org.eclipse.jdt.core.jdom.

Pakken org.eclipse.emf.codegen.merge.java

Klasse Beskrivelse
JControlModel En kontrolmodel, der leverer ordbøger og regler, som driver en fletteproces.
JMerger En klasse til fletning af Java-kildefiler. Bruger implementering af grænsefladerne i pakken org.eclipse.emf.codegen.merge.java.facade til at analysere kildekoden. Klassen kan bruges af programkode.
JPatternDictionary En ordbog med signaturer og Java-noder.

Pekken org.eclipse.emf.codegen.merge.properties

Klasse Beskrivelse
PropertyMerger En klasse til fletning af egenskabsfiler. Klassen kan bruges af programkode.

Udfør CodeGen som et hovedløst Eclipse-program

Klassen org.eclipse.emf.codegen.CodeGen kan omdanne en JET-skabelon til Java-kildekode og eventuelt flette Java-kildekoden til skabelonimplementering sammen med en eksisterende Java-klasse. CodeGen kan bruges som et hovedløst Eclipse-rogram ("hovedløst" betyder, at brugergrænsefladen i Eclipse ikke startes). Folderen plugins/org.eclipse.emf.codegen/test i Eclipse-installationen indeholder nogle scripts, der kan bruges til at starte klassen CodeGen som et hovedløst Eclipse-program. Disse scripts er i UNIX-format.

Nedenfor ses et eksempel på et script til Windows. Bemærk, at der overføres to argumenter til klassen CodeGen:

Hvis målstien allerede indeholder et resultat af en tidligere omdannelse, og du vil flette det nye omdannelsesresultat med det eksisterende, kan du angive kontrolmodelfilen JMerge som det tredje argument. Folderen plugins/org.eclipse.emf.codegen/test i Eclipse-installationen indeholder et eksempel på en merge.xml-fil.

   @echo off
   set ECLIPSE_HOME=C:\eclipse-2.1\eclipse
   set WORKSPACE=%ECLIPSE_HOME%\workspace
   set OPTS=-Xmx900M -Djava.compiler=NONE -verify -cp %ECLIPSE_HOME%\startup.jar
   set MAIN=org.eclipse.core.launcher.Main -noupdate -data %WORKSPACE% 
 
set TEMPLATE_URI=test.javajet
set TARGET_FOLDER=C:\temp\jetstandalone\MyProject
   set ARGUMENTS=%TEMPLATE_URI% %TARGET_FOLDER%
   
   echo Shut down Eclipse before running this script.
   java %OPTS% %MAIN% -application org.eclipse.emf.codegen.CodeGen %ARGUMENTS%

jetc: An ANT Task for Translating JET Templates Outside of Eclipse

Forfatter: Knut Wannheden (knut.wannheden snabel-a paranor.ch)

Binært format: jetc-task.jar.

Kilden: JETCTask.java.

Nogle bemærkninger:

Her er en simpel Ant-byggefil (taskdef-classpath forudsætter, at du har Eclipse 3.3 og EMF 2.3.0):

<project default="jetc_multiple_templates">
  <property name="eclipse.plugins.dir" location="C:\eclipse-SDK-3.3M4-win32\eclipse\plugins" />
    
  <taskdef name="jetc" classname="ch.paranor.epla.structure.JETCTask">
    <classpath>
      <pathelement location="jetc-task.jar" />
      <fileset dir="${eclipse.plugins.dir}">
        <include name="org.eclipse.core.boot_*.jar" />
        <include name="org.eclipse.core.resources_*.jar" />
        <include name="org.eclipse.core.runtime_*.jar" />
        <include name="org.eclipse.jdt.core_*.jar" />

        <include name="org.eclipse.emf.codegen_*.jar" />
      </fileset>
    </classpath>
  </taskdef>

  <!-- Usage example 1: -->
  <!-- Specify the template file in the "template" attribute. -->
  <!-- You can use the "class" and "package" attributes to override the -->
  <!-- "class" and "package" attributes in the template file. -->
  <target name="jetc_single_template">
    <mkdir dir="jet-output" />
    <jetc template="test.xmljet"
          package="com.foo"
          class="Test"
          destdir="jet-output" />
    <javac srcdir="jet-output" destdir="classes" />
  </target>

  <!-- Usage example 2: -->
  <!-- Translate a bunch of template files at once. -->
  <!-- You cannot use the "class" and "package" attributes when using a fileset. -->
  <target name="jetc_multiple_templates">
    <mkdir dir="jet-output" />
    <jetc destdir="jet-output">
      <fileset dir="jet-templates" includes="*.*jet" />
    </jetc>
    <javac srcdir="jet-output" destdir="classes" />
  </target>
</project>

Ressourcer

Substitutes for Missing C Constructs (By Joshua Bloch)

Java Tip 122: Beware of Java typesafe enumerations (By Vladimir Roubtsov)

Java Tip 133: More on typesafe enums (By Philip Bishop)

http://www.eclipse.org/emf/

Java and all Java-based trademarks and logos are trademarks or registered trademarks of Sun Microsystems, Inc. in the United States, other countries, or both.