javabog.dk  |  << forrige  |  indhold  |  næste >>  |  programeksempler  |  om bogen

9 Javabønner i JSP-sider


9.1 Javabønner 164

9.1.1 Bruge javabønner fra en JSP-side 164

9.1.2 Ønskeseddel med ArrayList som javabønne 165

9.2 At definere en javabønne og bruge den 166

9.2.1 Pakker og filplaceringer for klasser 166

9.2.2 Egenskaber på en javabønne 166

9.2.3 Sætte egenskaber fra en JSP-side 167

9.2.4 Aflæse bønnens egenskaber 168

9.2.5 Virkefelter for javabønner 168

9.2.6 Avanceret: Virkemåden af <jsp:useBean... /> 169

9.2.7 Initialisering af javabønner 170

9.2.8 Avanceret: Virkemåden af <jsp:setProperty .../> 170

9.3 Egenskaber på en javabønne 171

9.3.1 Indekserede egenskaber 171

9.4 Eksempel: En dagskalender 173

9.4.1 Udseende 173

9.4.2 Programmets virkemåde 174

9.4.3 HTML-siden (kalender.jsp) 175

9.4.4 Bruger-bønnen (Bruger.java) 176

9.4.5 Kalender-objektet (Kalender.java) 177

9.5 Eksempel: Login og brugeroprettelse 179

9.5.1 Brug af eksemplet 179

9.5.2 Login-bønnen 179

9.5.3 Login-siden 185

9.5.4 Brugeroprettelsen 186

9.5.5 En beskyttet side 188

9.5.6 Før eksemplet kan køre på din egen server 188

9.5.7 Opgaver 189

9.6 Test dig selv 190

9.7 Resumé 190


Dette kapitel forudsættes i resten af bogen. Det forudsætter kapitel 4, Videre med JSP, og kapitel 5, Brug af databaser.

Det er ønskværdigt at adskille tekstligt indhold og programkode fra hinanden, sådan at f.eks. en HTML-designer kan koncentrere sig om JSP-sidernes HTML-layout og indhold, mens en programmør koncentrerer sig om den bagvedliggende kode.

Et af de vigtigste elementer i sådan en struktur er, at programkode og datastrukturer lægges for sig i separate javaklasser, som så kan bruges fra JSP-siderne. Disse javaklasser anvendes derefter som javabønner fra JSP-siderne.

9.1 Javabønner

Enhver java-klasse kan kaldes en javabønne (eng.: Java Bean), bare to krav er opfyldt:

De fleste klasser - både systemets og dem du selv definerer - kan altså opfattes som javabønner.

9.1.1 Bruge javabønner fra en JSP-side

Den mest almindelige måde at bruge en javabønne på er, at knytte den til brugerens session, sådan at den følger med brugeren. Det gøres med JSP-koden:

<jsp:useBean id="bønnenavn" class="pakkenavn.Klassenavn" scope="session" />

Eksempelvis kunne vi knytte en ArrayList (eller Vector) til brugerens session med:

<jsp:useBean id="liste" class="java.util.ArrayList" scope="session" />

Derefter kan ArrayList-objektet bruges under navnet 'liste'.

Attributten scope="session" betyder, at objektet har sessionen som virkefelt, d.v.s. at objektet vil blive husket og genbrugt næste gang brugeren besøger siden og at hver bruger vil have sit eget objekt (som beskrevet i afsnit 4.1, Sessioner).

<jsp:useBean /> bruges ikke kun til at oprette et objekt, det bruges også til efterfølgende at genbruge objektet

Man kan også knytte objekter til andet end sessionen (beskrives senere, i afsnit 9.2.5).

9.1.2 Ønskeseddel med ArrayList som javabønne

Her ses ønskeseddel-eksemplet fra afsnit 4.1.1 igen, blot ændret til at bruge klassen ArrayList som en javabønne, der er knyttet til brugerens session (ændringerne er i fed).

<html>
<head><title>Ønskeseddel - ArrayList som javabønne</title></head>
<body>
Dette eksempel demonstrerer, hvordan et ArrayList-objektet kan knyttes til
brugerens session som en javabønne.

<jsp:useBean id="ønsker" class="java.util.ArrayList" scope="session" />

<h3>Skriv et ønske</h3>
Skriv noget, du ønsker.
<form>
<input type="text" name="oenske">
</form>
<%
  // se om der kommer en parameter med endnu et ønske
  String ønske = request.getParameter("oenske");
  if (ønske != null) {
    ønsker.add(ønske);                       // tilføj ønske til listen
  }

  if (ønsker.size()>0) {                     // udskriv ønsker i listen
    %>
      <h3>Ønskeseddel</h3>
      Indtil nu har du følgende ønsker:<br>
    <%
    // udskriv hele listen
    for (int i=0; i<ønsker.size(); i++) 
    { %>
      Ønske nr. <%= i %>: <%= ønsker.get(i) %><br>
    <% }
  }
%>
</body>
</html>

Sammenlignet med det oprindelige eksempel i afsnit 4.1.1 ser man, at al arbejdet med at hente listen ud af brugerens session (og evt. oprette listen) er sparet væk.

9.2 At definere en javabønne og bruge den

Lad os nu se på, hvordan man definerer sine egne javabønner. Vi definerer klassen Person:

Person.java

package minPakke; //oversat fil skal ligge i WEB-INF/classes/minPakke/Person.class

public class Person
{
  private String fornavnet;           // intern variabel (ikke egenskab)
  private int alderen;                // intern variabel (ikke egenskab)

  public Person()                     // konstruktør uden parametre skal eksistere
  {
  }

  public String getFornavn()          // aflæser egenskaben fornavn
  {
    return fornavnet;
  }

  public void setFornavn(String n)    // sætter egenskaben fornavn
  {
    fornavnet = n;
  }

  public int getAlder()               // aflæser egenskaben alder
  {
    return alderen;
  }

  public void setAlder(int n)         // sætter egenskaben alder
  {
    alderen =  n;
    System.out.println("Alder sat til: "+n); // udskriv ændring til loggen
  }
}

9.2.1 Pakker og filplaceringer for klasser

Vil man bruge en klasse fra en JSP-side, skal klassen defineres i en pakke1. Derfor er bønnen defineret i pakken minPakke.

Webserveren leder kun efter javaklasser i undermappen WEB-INF/classes/. Klasserne skal derfor ligge dette specielle sted, for at kunne bruges fra JSP-siderne.

Således skal den oversatte klasse, Person.class, ligge i WEB-INF/classes/minPakke/ i den webapplikation klassen tilhører.

9.2.2 Egenskaber på en javabønne

Ofte har en javabønne nogle get- og set-metoder, som kan ændre i objektets data. Sådanne metoder kaldes også egenskaber (eng.: properties).

Herunder har vi for eksempel bønnen Person med metoderne getFornavn() og setFornavn(), svarende til egenskaben fornavn og de tilsvarende metoder for egenskaben alder.

Læg mærke til, at bønnen har egenskaben alder af typen int, fordi den har metoderne

  public int getAlder()
  public void setAlder(int n)

Hvordan data gemmes internt er underordnet (i dette tilfælde gemmes det i en privat variabel kaldet alderen - data kunne lige så godt være lagret på en anden måde, f.eks. i form af fødselsåret).

Tilsvarende har bønnen egenskaben fornavn af typen String, fordi den har metoderne

  public String getFornavn()
  public void setFornavn(String n)

Serveren kan automatisk kalde egenskabernes set-metoder, hvis der kommer data fra en formular, hvor parameternavnene passer med bønnens egenskaber.

9.2.3 Sætte egenskaber fra en JSP-side

En JSP-side, der bruger bønnen, kunne have en formular med parametrene fornavn og alder overført. Den kunne se således ud:

<html>
<head><title>En simpel javabønne</title></head>
<body>
<jsp:useBean id="person" class="minPakke.Person" scope="session" />
<jsp:setProperty name="person" property="*" />

<% if (person.getFornavn() == null) { %>
  Indtast dit fornavn og din alder:
  <form>
    <input type="text" name="fornavn">
    <input type="text" name="alder" size="4">
    <input type="submit" value="OK">
  </form>
<% } else { %>
  Hej <%= person.getFornavn() %>!
  Din alder er: <%= person.getAlder() %>.
<% } %>
<br>Til den <a href="en_simpel_javaboenne_2.jsp">anden side</a>.
</body>
</html>

Udfylder man formularen, ser man, at dens indhold kommer over i javabønnen (i serverens log kan man også se at metoden setAlder() bliver kaldt).

Det skyldes linjen, der aflæser parametrene (fornavn og alder) og kalder set-metoder i bønnen (setFornavn() og setAlder()):

<jsp:setProperty name="person" property="*" />

Faldgruber

Når du definerer egenskaber og vil binde dem til formularfelter, så husk:

Formularfelter bør altid skrives med lille startbogstav. De tilsvarende metoder i bønnen bør altid skrives med stort startbogstav efter get/set

Forklaringen er, at formularfeltet (som bliver navnet på parameteren og egenskaben) har stort startbogstav i metodenavnet efter get/set. Hedder egenskaben f.eks. 'fornavn' kommer metoderne i bønnen til at hedde getFornavn() og setFornavn().

Brug derfor små bogstaver i formularfeltet, f.eks :

  <input type="text" name="fornavn">  <%-- rigtigt --%>

Brug aldrig f.eks.

  <input type="text" name="Fornavn">  <%-- forkert! --%>

og heller ikke:

  <input type="text" name="FORNAVN">  <%-- forkert! --%>

En anden hyppig fejltagelse er at glemme formen af metoderne beskrevet i afsnit 9.2.2.

9.2.4 Aflæse bønnens egenskaber

Egenskaber kan også aflæses for bønnen og indsættes i HTML-koden. Med f.eks.:

Din alder er: <jsp:getProperty name="person" property="alder" />.

bliver værdien af bønnens egenskab alder sat ind.

Det er kortere simpelt hen at skrive f.eks.:

Din alder er: <%= person.getAlder() %>.

Derfor ses koden <jsp:getProperty ... /> i praksis ikke så ofte.

9.2.5 Virkefelter for javabønner

Her er en anden side, der også bruger Person-bønnen.

<html>
<head><title>En simpel javabønne 2</title></head>
<body>
<jsp:useBean id="person" class="minPakke.Person" scope="session" />

Hej igen, <%= person.getFornavn() %>.<br>
Dette er en anden side, der også bruger javabønnen person.<br>
Din alder er i øvrigt <%= person.getAlder() %> år.<br>
Til den <a href="en_simpel_javaboenne.jsp">første side</a>.
</body>
</html>

Besøger man denne, vil man se, at dataene fra en_simpel_javaboenne.jsp er gemt:

Det skyldes, at bønnen har sessionen som virkefelt (eng.: scope). Objektet vil blive husket i brugerens session og genbrugt, når brugeren besøger andre sider, der har koden

<jsp:useBean id="person" class="minPakke.Person" scope="session" />

Oversigt over de mulige virkefelter

Virkefeltet scope="session", der knytter bønnen til session-objektet (se afsnit 4.5.4, session - objekt der følger den enkelte bruger) er det mest anvendte, men der findes også andre virkefelter.

Eksempelvis vil scope="application", gemme bønnen i application-objektet (se afsnit 4.5.5, application - fælles for hele webapplikationen) og dermed give bønnen hele applikationen som virkefelt, dvs. at den samme bønne deles mellem alle brugere og alle sider (analogt til en global variabel).

Derudover kan en javabønne have virkefeltet scope="request". Da knyttes bønnen til den aktuelle anmodning (til request-objektet, se afsnit 4.5.1, request - anmodningen fra klienten) og bliver smidt væk, når anmodningen er fuldført. Det kan bruges ved server-omdirigering fra en side til en anden (se afsnit 4.3.2, Server-omdirigering).

Det mest kortlivede virkefelt er scope="page", der knytter bønnen til page-objektet. Bønnen eksisterer indtil udførelsen af den aktuelle side er fuldført (omdirigeres der til andre sider smides bønnen væk). Den har altså samme levetid som en almindelig variabel erklæret i JSP-siden imellem <% og %>.

9.2.6 Avanceret: Virkemåden af <jsp:useBean... />

Dette afsnit er ikke omfattet af Åben Dokumentslicens.
Du skal købe bogen for at måtte læse dette afsnit.
Jeg erklærer, at jeg allerede har købt bogen
Jeg lover at anskaffe den i nær fremtid.

9.2.7 Initialisering af javabønner

Ofte ønsker man at få noget programkode kørt én gang når javabønnen initialiseres. Det kan man gøre ved at lægge koden mellem <jsp:useBean> og </jsp:useBean>. Herunder skriver vi ud hver gang et person-objekt bliver oprettet og sætter fornavnet til "(ukendt)":

  <jsp:useBean id="person" class="minPakke.Person" scope="session" >
    <%
      out.print("Et nyt person-objekt blev oprettet!");
      person.setFornavn("(ukendt)");
    %>
  </jsp:useBean> 

Et eksempel på dette er vist i afsnit 10.4.4.

9.2.8 Avanceret: Virkemåden af <jsp:setProperty .../>

Dette afsnit er ikke omfattet af Åben Dokumentslicens.
Du skal købe bogen for at måtte læse dette afsnit.
Jeg erklærer, at jeg allerede har købt bogen
Jeg lover at anskaffe den i nær fremtid.

9.3 Egenskaber på en javabønne

Dette afsnit er ikke omfattet af Åben Dokumentslicens.
Du skal købe bogen for at måtte læse dette afsnit.
Jeg erklærer, at jeg allerede har købt bogen
Jeg lover at anskaffe den i nær fremtid.

9.3.1 Indekserede egenskaber

Derudover kan egenskaber være indekseret. En egenskab siges at være indekseret, hvis HTML-sidens formular har flere parametre, der hedder det samme.

Herunder er et eksempel på brug af indekserede egenskaber.

<html>
<head><title>Indekserede egenskaber</title></head>
<body>
<jsp:useBean id="ie" class="minPakke.IndekseredeEgenskaber" scope="session" />
<jsp:setProperty name="ie" property="*" />
Indtast et antal fornavne og aldre:
<form>
  <input type="text" name="fornavn"> <input type="text" name="alder"><br>
  <input type="text" name="fornavn"> <input type="text" name="alder"><br>
  <input type="text" name="fornavn"> <input type="text" name="alder"><br>
  <input type="text" name="fornavn"> <input type="text" name="alder"><br>
  <input type="submit" value="OK">
</form>
<br><br>
Resultatet udskrives i serverens log.
</body>
</html>

Javabønnen har defineret de indekserede egenskaber fornavn og alder til at modtage parametrene:

IndekseredeEgenskaber.java

package minPakke;
public class IndekseredeEgenskaber
{
  public void setFornavn(String[] arr)  // sætter indekseret egenskab fornavn
  {
    for (int i = 0; i < arr.length; i++) {
      System.out.println("setFornavn(arr["+i+"] = "+arr[i]+")");
    }
  }

  public void setAlder(int[] arr)       // sætter indekseret egenskab alder
  {
    for (int i = 0; i < arr.length; i++) {
      System.out.println("setAlder(arr["+i+"] = "+arr[i]+")");
    }
  }
}

Lad os nu forestille os at brugeren udfylder formularen med f.eks.:




Da vil javabønnen udskrive i serverens log (for Tomcat hedder den logs/catalina.out):

setAlder(arr[0] = 33)
setAlder(arr[1] = 31)
setAlder(arr[2] = 15)
setAlder(arr[3] = 0)
setFornavn(arr[0] = Jacob)
setFornavn(arr[1] = Anne Mette)
setFornavn(arr[2] = Gülsen)
setFornavn(arr[3] = Aske)

9.4 Eksempel: En dagskalender

Dette eksempel er et mindre program, der viser en dagskalender og giver mulighed for at redigere i den.

9.4.1 Udseende

Som udgangspunkt ser kalenderen således ud:

Klikker man på "rediger" kommer samme skærmbillede frem, men denne gang med indtastningsfelter så man kan rette i teksten:

9.4.2 Programmets virkemåde

Programmet består af JSP-siden kalender.jsp og klasserne Bruger.java og Kalender.java.

Klassen Bruger er en javabønne, der knyttes til brugerens session. Hver bruger får derfor sit eget Bruger-objekt. Alle Bruger-objekterne kommunikerer med det samme Kalender-objekt (derved bliver det en fælles kalender).

Kalender-objektet repræsenterer logikken i programmet og er ikke specielt rettet mod en webapplikation, men kunne også bruges i andre sammenhænge.

Her er klassediagrammet (private metoder og variabler er ikke vist):

9.4.3 HTML-siden (kalender.jsp)

Først JSP-siden som brugeren ser:

kalender.jsp

<%-- Knyt bønnen Bruger til brugerens session under navnet b --%>
<jsp:useBean id="b" class="kalender.Bruger" scope="session" />

<%-- Overfør parametre til b, der svarer til egenskaber --%>
<jsp:setProperty name="b" property="*" />

<html>
<head><title>Kalender</title></head>
<body>

<h1>Fælleskalender for <%= b.getDatostr() %></h1>

<a href="kalender.jsp?maaned=<%= b.getMaaned()-1 %>">forrige</a> måned -
<a href="kalender.jsp?maaned=<%= b.getMaaned()+1 %>">næste</a> måned -
<% 
  if (b.isRediger()) { // redigering - vis en input-formular
%>

  <a href="kalender.jsp?rediger=false">vis</a> kalenderen.<br>
  <br>
  <form action="kalender.jsp" method="post">
    Tryk <input type="submit" value="OK"> når du vil gemme ændringer.<br>

    <% b.udskrivDagsprogram(out); %>

    <br><br>
    <input type="submit" value="OK">
  </form>
<%
  } else {             // fremvisning
%>
  <a href="kalender.jsp?rediger=true">redigér</a> kalenderen.<br>

  <% b.udskrivDagsprogram(out); %>
<%
  } 
%>
</body>
</html>

Bemærk hvordan vi definerer variablen b af type Bruger og opretter et objekt, der genbruges i hele den kommende session:

<jsp:useBean id="b" class="kalender.Bruger" scope="session" />

JSP-siden benytter Bruger-bønnens egenskaber rediger, maaned og dagsprogram. Vi sørger for at egenskaber på b bliver sat, hvis de tilsvarende parametre overføres til siden med:

<jsp:setProperty name="b" property="*" />

der (j.v.f. afsnit 9.2.6) svarer til koden:

  String parm1 = request.getParameter("rediger");
  if (parm1 != null) b.setRediger( parm1.equals("true") );

  String parm2 = request.getParameter("maaned");
  if (parm2 != null) b.setMaaned( Integer.parseInt(parm2) );

  String[] parm3 = request.getParameterValues("dagsprogram");
  if (parm3 != null) b.setDagsprogram  ( parm3 );

9.4.4 Bruger-bønnen (Bruger.java)

Herunder er Bruger-bønnens. Metoder der svarer til egenskaber er fremhævet med fed:

Bruger.java

package kalender;
import java.util.*;
import java.text.*;
import java.io.*;

public class Bruger {
  private SimpleDateFormat månedFormat;
  private SimpleDateFormat dagugedagFormat;

  public void setLocale(Locale sproget) { 
    månedFormat = new SimpleDateFormat("MMMM yyyy",sproget); // f.x. 'maj 2004'
    dagugedagFormat = new SimpleDateFormat("dd EE",sproget); // f.x. '31 ma'
  }

  // tom konstruktør - ellers er det ikke en javabønne
  public Bruger() { setLocale(new Locale("da","DK")); }

  private boolean redigering;
  public void setRediger(boolean r) { redigering = r; }
  public boolean isRediger() { return redigering; }

  private GregorianCalendar dato = new GregorianCalendar();

  public void setMaaned(int m) {
    dato.set(Calendar.MONTH, m);
    dato.set(Calendar.DAY_OF_MONTH, 1); // første dag, så hele måneden ses
  }
  public int getMaaned() { return dato.get(Calendar.MONTH); }

  /** Giver aktuelle måned og år som en streng */
  public String getDatostr() { return månedFormat.format(dato.getTime()); }

  /** Egenkaben dagsprogram er et array af strenge, en for hver dag.
   *  der kaldes videre i det fælles Kalender-objekt                   */
  public void setDagsprogram(String[] dagsprogram) {
    redigering = false;
    int start = Kalender.instans.beregnIndex(dato);
    for (int i=0; i<dagsprogram.length; i++)
      Kalender.instans.sætDagstekst(start+i,dagsprogram[i]);
  }

  /** Producerer HTML-kode der viser et dagsprogram */
  public void udskrivDagsprogram(Writer out) throws IOException {
    GregorianCalendar kal = (GregorianCalendar) dato.clone();
    int start = Kalender.instans.beregnIndex(kal);
    int antal = 1 + dato.getActualMaximum(Calendar.DAY_OF_MONTH)
                  - dato.get(Calendar.DAY_OF_MONTH);
    for (int i=0; i<antal; i++) {
      String dagugedag = dagugedagFormat.format(kal.getTime());
      out.write("<br>\n<code>");
      out.write(dagugedag);
      out.write("</code> ");
      if (!redigering) out.write(Kalender.instans.hentDagstekst(i+start));
      else out.write("<input type='text' size=30 name='dagsprogram' value='"
        + Kalender.instans.hentDagstekst(i+start).replace('\'','\"') + "'>");
      kal.roll(Calendar.DAY_OF_MONTH,1);
    }
  }
}

Bemærk hvordan egenskaben dagsprogram svarer til metoden setDagsprogram() og inputfeltet i HTML-formularen med navnet dagsprogram. Egenskaben er et eksempel på en indekseret egenskab, da den sættes med et array (se afsnit 9.3.1, Indekserede egenskaber).

Selve HTML-koden til dagsprogrammet bliver ikke produceret af JSP-siden. I stedet kaldes metoden udskrivDagsprogram() med out-objektet som parameter på Bruger-objektet, der så producerer HTML-koden.

Man kunne også havde ladet JSP-siden producere HTML-koden til kalenderen (det ville nok være bedre stil, men ville også gøre eksemplet sværere at overskue).

9.4.5 Kalender-objektet (Kalender.java)

Den anden klasse er et Kalender-objekt, der fungerer som data-lager og husker aftalerne.

Kalender.java

package kalender;

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

public class Kalender
{
  private boolean ændret;
  private List liste;
  /**
   * @param kal Datoen vi ønsker at kende indekset i listen på
   * @returns indekset der skal bruges i kald til sæt() og hent().
   */
  public int beregnIndex(Calendar kal) {
    int år = kal.get(Calendar.YEAR);
    int dag = kal.get(Calendar.DAY_OF_YEAR);
    // det vigtigste er at to dage aldrig får samme indeks
    return (år-2003)*366+dag;
  }

  /**
   * Sæt teksten for en bestemt dag.
   * @see #beregnIndeks(Calendar)
   * @param indeks Indekset i listen. Skal først findes med beregnIndeks()
   * @param tekst Teksten for dagen
   */
  public void sætDagstekst(int indeks, String tekst) {
    // Fyld op med tomme strenge hvis der gås ud over listen
    while (indeks>=liste.size()) liste.add("");
    liste.set(indeks,tekst);
    ændret = true;
  }

  /**
   * Hent teksten for en bestemt dag.
   * @see #beregnIndeks(GregorianCalendar)
   * @param index Indekset i listen. Skal først findes med beregnIndeks()
   * @return tekst Teksten for dagen
   */
  public String hentDagstekst(int indeks) {
    if (indeks<0 || liste.size()<=indeks) return "";
    else return (String) liste.get(indeks);
  }

  public static final Kalender instans = new Kalender(); // singleton med

  private Kalender() {                                   // privat konstruktør
    try { // indlæs kalenderen fra XML-fil på disken (hvis den findes)
      XMLDecoder kal = new XMLDecoder(new FileInputStream("kalender.xml"));
      // alternativ: hent serialiseret objekt i stedet for XML-data
      //ObjectInputStream kal = new ObjectInputStream(
      //                        new FileInputStream("kalender.ser"));
      liste = (ArrayList) kal.readObject();
      kal.close();
      System.out.println("Kalender indlæst: "+liste);
    } catch (Exception e) {
      System.out.println("Kalender ikke indlæst, opretter ny: "+e);
      liste = new ArrayList();
    }
    GemRegelmæssigt g = new GemRegelmæssigt(); // gemmer kalenderen på disken
  }

  /** Sørger for at gemme kalenderen regelmæssigt (i en separat tråd) */
  class GemRegelmæssigt extends Thread
  {
    public GemRegelmæssigt() {
      setDaemon(true);   // systemet må godt stoppe selvom tråden stadig kører
      setPriority(MIN_PRIORITY);
      start();
    }

    public void run() {
      while (true) try {
        Thread.sleep(1*60*1000); // hvert minut,
        if (ændret) {            // hvis kalenderen er ændret
          ændret = false;        // gem kalenderen på disken
          // gem som XML
          XMLEncoder kal = new XMLEncoder(new FileOutputStream("kalender.xml"));
          // alternativ: gem som serialiseret objekt i stedet for XML
          //ObjectOutputStream kal = new ObjectOutputStream(
          //                         new FileOutputStream("kalender.ser"));
          kal.writeObject(liste);
          kal.close();
          System.out.println("Kalender gemt: "+liste);
        }
      } catch (Exception e) { e.printStackTrace(); }
    }
  }
}

Denne kalender er fælles for alle brugerne og er programmeret sådan, at der kun bliver oprettet ét objekt af typen Kalender (en singleton).

I stedet for at bruge en database henter og gemmer kalenderen en gang imellem sine data i en XML-fil (med navnet 'kalender.xml'). Formatet og indholdet af XML-filen diskuteres i afsnit 11.2.2, Nem generering af XML fra Java-objekter. Kommenteret væk er også kode til i stedet at gemme data serialiseret i en fil (med navnet 'kalender.ser').

9.5 Eksempel: Login og brugeroprettelse

Det følgende eksempel viser, hvordan man kan have beskyttede sider, hvor brugerne skal logge ind med brugernavn og adgangskode, defineret i en database.

Derudover kan nye brugere registrere sig, ved at opgive en epost-adresse, hvorefter en bruger vil blive oprettet i databasen og en adgangskode vil blive sendt til epost-adressen.

Hvis brugeren ønsker det, kan hans oplysninger gemmes i en cookie, sådan at han automatisk kan blive logget ind, næste gang han besøger siden.

9.5.1 Brug af eksemplet

Eksemplet er udformet således, at du kan tage det og bruge det i dit eget projekt.

Bemærk dog, at der i næsten alle webservere allerede findes foruddefinerede måder at lave beskyttede sider på, blot ved at ændre i konfigurationsfilerne, som beskrevet i afsnit 8.2.1, Containerstyret adgangskontrol.

Eksemplet her er derfor reelt kun nødvendigt at kopiere, hvis du ønsker f.eks. dynamisk brugeroprettelse med tilsending af adgangskoder eller du selv vil styre det præcise udseende af login-skærmbilledet eller en af de andre ting nævnt i afsnit 8.2, Adgangskontrol.

9.5.2 Login-bønnen

Først javabønnen Login.java. Den har egenskaberne brugernavn og adgangskode, der skal være korrekt sat før egenskaben ok bliver sand og brugeren får lov til at komme videre.

Metoderne, der svarer til egenskaber, er fremhævet med fed.

Login.java

package javabog;

import java.sql.*;
import java.util.*;
import javax.mail.*;
import javax.mail.internet.*;
import javax.servlet.*;
/**
 * Denne javabønne har ansvaret for registrering og logintjek af en bruger.
 * Husk at mail.jar, activation.jar og MySQL-driveren skal være i CLASSPATH.
 */
public class Login
{
  private String brugernavn = "";
  private String adgangskode = "";
  private String epost = "";
  private String meddelelse = "";       // fejlmeddelelse til brugeren

  private boolean tjek = false;         // om adgangskode skal tjekkes
  private boolean loggetInd = false;    // om adgangskoden var korrekt

  public void setBrugernavn(String bn)  { tjek=true; brugernavn=bn; }
  public String getBrugernavn()         { return brugernavn; }

  public void setAdgangskode(String ak) { tjek=true; adgangskode = ak; }

  public void setEpost(String epost)    { this.epost = epost; }
  public String getEpost()              { return epost; }

  public void setMeddelelse(String m)   { meddelelse = m; }
  public String getMeddelelse() { String m=meddelelse; meddelelse=""; return m; }

  /** Egenskaben loggetInd. Kan af sikkerhedsgrunde kun aflæses */
  public boolean isLoggetInd() {
    if (tjek) return false;       // er der sket ændringer skal der logges ind
    return loggetInd;
  }

  /** Forbindelsen til databasen. Oprettes når klassen indlæses */
  private static Connection con = null;
  private static String postserver = null;
  private static String postafsender = null;

  /** Initialisering. Skal kaldes før bønnen bruges */
  public synchronized static void init(ServletContext application)
  {
    if (con!=null) return; // initialisér kun hvis det ikke allerede er gjort

     // Opret forbindelse til databasen når klassen bruges første gang
    try {
      String drv=null, url=null, bru=null, adg=null;
      if (application!=null) {
        drv = application.getInitParameter("dbDriver");
        url = application.getInitParameter("dbUrl");
        bru = application.getInitParameter("dbBruger");
        adg = application.getInitParameter("dbAdgangskode");
        postserver = application.getInitParameter("postserver");
        postafsender = application.getInitParameter("postafsender");
      }
      if (drv==null) drv = "com.mysql.jdbc.Driver";
      if (url==null) url = "jdbc:mysql:///test";
      if (bru==null) bru = "root";
      if (adg==null) adg = "";

      System.out.println("url="+url+" bru="+bru+" adg="+adg); // til fejlfinding
      Class.forName(drv);                                  // indlæs driver
      con = DriverManager.getConnection(url, bru, adg);    // opret forbindelse

      Statement s = con.createStatement();

      try { // opret tabellen (dette giver en fejl hvis den allerede eksisterer)
        s.executeUpdate("CREATE TABLE brugere (brugernavn varchar(20), "+
                        "adgangskode varchar(20), epost varchar(50))");

        // Indsæt så også nogle brugere, så der er nogle til at starte med
        s.executeUpdate("INSERT INTO brugere VALUES ('Jacob','hemli','')");
        s.executeUpdate("INSERT INTO brugere VALUES ('Preben','hemli','')");
        s.executeUpdate("INSERT INTO brugere VALUES ('Søren','hemli','')");
        System.out.println("Bemærk: Standardbrugere oprettet (sikkerhedshul!)");
      } catch (SQLException sqlex) {} // OK med fejl her, log dem derfor ikke

      ResultSet rs = s.executeQuery("SELECT brugernavn FROM brugere");
      while (rs.next()) System.out.println( "bruger: "+rs.getString(1) );
      rs.close();
      s.close();
    }
    catch (Exception ex) {
      System.out.println("Problem med databasen: "+ex);
      ex.printStackTrace(); // Kritisk fejl, log den
      con = null; // sæt forbindelsen til null og prøv at oprette den senere
    }
  }

  /** Tjekker om brugernavn og adgangskode er OK */
  public void tjekLogin()
  {
    if (!tjek) return; // er der ikke sket ændringer behøver vi ikke tjekke igen
    loggetInd = false;
    tjek = false;
    if (brugernavn.length() > 0 && adgangskode.length() > 0) try {
      if (con == null) init(null); // ingen forbindelse - forsøg at oprette en
      PreparedStatement s = con.prepareStatement(
        "SELECT brugernavn FROM brugere WHERE brugernavn=? AND adgangskode=?");
      s.setString(1, brugernavn);
      s.setString(2, adgangskode);
      ResultSet rs = s.executeQuery();
      if (rs.next()) loggetInd = true; // korrekt! Brugeren er logget ind.
      else meddelelse = "Forkert brugernavn eller adgangskode";

      rs.close();
      s.close();
    } catch (Exception e) {
      e.printStackTrace();
      meddelelse = "Kunne ikke logge ind: " + e;
    }
  }

  /**
   * Registrerer ny bruger og sender adgangskode til epost-adressen
   * @return true hvis oprettelsen gik godt, ellers false.
   */
  public boolean opretBruger()
  {
    tjek = false;
    loggetInd = false;
    if (brugernavn.length() >= 1 && epost.length() >= 5 ) try {
      String nyAdgangskode = lavNyAdgangskode();
      PreparedStatement s = con.prepareStatement(
        "INSERT INTO brugere VALUES (?,?,?)");
      s.setString(1, brugernavn);
      s.setString(2, nyAdgangskode);
      s.setString(3, epost);
      s.executeUpdate();
      s.close();
      sendAdgangskode(nyAdgangskode);
      return true;             // oprettelse lykkedes!
    } catch (Exception e) {
      e.printStackTrace();
      meddelelse = "Fejl under oprettelsen: "+e;
    } else {
      meddelelse = "Brugernavn og epost skal være udfyldt";
    }
    return false;
  }

  /**
   * adgangskode genereres til ny bruger
   * @return koden
   */
  private String lavNyAdgangskode()
  {
    String ord = "";
    for (int j=0; j<6; j++) {
      ord = ord + (char) ('a' + (char) (Math.random()*25));
    }
    return ord;
  }

  /**
   * Send adgangskoden til brugeren med e-post
   */
  private void sendAdgangskode(String adgangskoden) throws Exception 
  {
    if (postafsender==null) postafsender = "din@adresse.dk";
    if (postserver==null) postserver = "post.tele.dk";

    Properties prop = new Properties();
    prop.setProperty("mail.host", postserver);   // afhænger af internetudbyder
    prop.setProperty("mail.transport.protocol", "smtp");
    Session session = Session.getInstance(prop);

    // Opbyg beskedden
    Message besked = new MimeMessage(session);
    besked.setFrom(new InternetAddress(postafsender));
    besked.setRecipient(Message.RecipientType.TO, new InternetAddress(epost));
    besked.setSubject("Din adgangskode til javabog.dk");
    String txt = "Din adgangskode til javabog.dk er: "+adgangskoden+"\n"
      + "Du kan også logge ind ved at klikke på nedenstående henvisning:\n"
      + "http://javabog.dk:8080/JSP/kode/kapitel_09/log_ind.jsp?brugernavn="
      + java.net.URLEncoder.encode(brugernavn,"UTF-8")+"&adgangskode="
      + java.net.URLEncoder.encode(adgangskoden,"UTF-8")+"&handling=log+ind";
    besked.setContent(txt, "text/plain"); // put beskedden ind
    System.out.println(txt);              // skriv også beskedden i serverens log

    Transport.send(besked);               // send beskedden
    meddelelse = "Adgangskoden er sendt til adressen "+ epost;
  }
}

Metoden init() skal kaldes før javabønnen tages i brug. Den initialiserer forbindelsen til databasen (og opretter for nemheds skyld bruger-tabellen, hvis den ikke allerede findes)2.

Metoden forventer at application-objektet bliver overført, sådan at den kan aflæse initialiseringsparametrene dbDriver, dbUrl, dbBruger, dbAdgangskode fra web.xml, som beskrevet i afsnit 4.5.5 (der er dog nogle fornuftige standardværdier i koden, hvis application-objektet ikke overføres eller initialiseringsparametrene ikke er defineret).

Metoden tjekLogin() tjekker, ved at kontakte databasen, om brugeren har angivet korrekt brugernavn og adgangskode. Af sikkerhedsgrunde bruger den PreparedStatement, for at undgå SQL-injektioner (se afsnit 5.4.2).

Resultatet kan aflæses med metoden isLoggetInd(), der svarer til egenskaben loggetInd.

Metoden sendAdgangskode() bruger JavaMail til at sende adgangskoden til brugeren med epost. For at det virker i din webapplikation skal du have sat initialiseringsparametren "postserver" til at pege på din internetudbyders SMTP-server.

9.5.3 Login-siden

Her kommer log_ind.jsp, skærmbilledet, hvor brugeren kan logge ind:


log_ind.jsp

<jsp:useBean id="login" class="javabog.Login" scope="session">
  <% login.init(application); %>  <%-- køres første gang bønnen bruges --%>
</jsp:useBean>

<%
  // Hvis brugernavn og kode er sat i en cookie (se afsnit 3.6.5) så brug dem:
  Cookie[] cookier = request.getCookies();
  if (cookier != null) for (int i=0; i<cookier.length; i++) {
    Cookie c = cookier[i];
    System.out.println("cookie "+c.getName()+"="+c.getValue());
    if (c.getName().equals("brugernavn")) login.setBrugernavn(c.getValue());
    if (c.getName().equals("adgangskode")) login.setAdgangskode(c.getValue());
  }
  // Hvis brugernavn og kode kommer med request-objektet så sæt dem:
%>
<jsp:setProperty name="login" property="brugernavn"/>
<jsp:setProperty name="login" property="adgangskode"/>

<html>
<head><title>Log ind</title></head>
<body>

<img src="../kapitel_02/billede_med_avatar.jsp" align="right">
<h1>Log ind</h1>

<form method="post" action="log_ind.jsp">
<table>
<tr>
  <td>Brugernavn:</td>
  <td><input type="text" name="brugernavn" value="<%=login.getBrugernavn()%>"></td>
</tr>
<tr>
  <td>Adgangskode:</td>
  <td><input type="password" name="adgangskode"></td>
</tr>
</table>

<input type="submit" name="handling" value="log ind">
    <input type="checkbox" name="saet cookie">Husk mig på denne computer<br>
Jeg er <a href="ny_bruger.jsp">ny bruger</a> og ønsker at registrere mig.
</form>
<p>

<font color="red">
<%
login.tjekLogin();
if (login.isLoggetInd()) {
  if (request.getParameter("saet cookie") != null) {
    response.addCookie(
      new Cookie("brugernavn", request.getParameter("brugernavn")));
    response.addCookie(
      new Cookie("adgangskode", request.getParameter("adgangskode")));
  }
  %>
  Du er logget ind som <jsp:getProperty name="login" property="brugernavn"/>.
  <%
} else {
  String handling = request.getParameter("handling");
  if ("log ind".equals(handling)) {
    if (login.getBrugernavn().length()>0) {
      %>
      <%=login.getMeddelelse()%>
      Prøv igen.
  <%  } else { %>
      Indtast brugernavn og adgangskode.
      <%
    }
  }
}
%>
</font>

<a href="beskyttet_side.jsp">Gå til den beskyttede side</a>
</body>
</html>

9.5.4 Brugeroprettelsen

Hvis der er tale om en ny bruger vises dette skærmbillede, hvor brugeren kan vælge et brugernavn og angive sin epost-adresse, for at få oprettet en bruger.

For at undgå at alle mulige opretter brugere i flæng (eller endda laver et program, der gør det automatisk!), skal der også indtastes en sikkerhedskode, der bliver på et billede. Til det bruger vi eksemplet fra afsnit 2.8.5, til at producere et JPG-billede med sikkerhedskode. Teksten på billedet bestemmer vi ved at sætte attributten "billedtekst" i sessionen.

ny_bruger.jsp

<jsp:useBean id="login" class="javabog.Login" scope="session"/>
<jsp:setProperty name="login" property="brugernavn"/>
<jsp:setProperty name="login" property="epost"/>

<html>
<head><title>Ny bruger</title></head>
<body>

<h1>Registrering af ny bruger</h1>

<form action="ny_bruger.jsp" method="get">
<table>
<tr>
  <td>Ønsket brugernavn:</td>
  <td><input type="text" name="brugernavn" value="<%= login.getBrugernavn() %>">
  </td>
</tr>
<tr>
  <td>Epost:</td>
  <td><input type="text" name="epost" value="<%= login.getEpost() %>"></td>
</tr>
<tr>
  <td>Indtast sikkerhedskode</td>
  <td><input type="text" name="sikkerhedskode"></td>
</tr>
</table>

<% // Ekstra sikkerhedskode som bruger skal aflæse fra billede
  String sikkerhedskode = (String) session.getAttribute("sikkerhedskode");
  if (sikkerhedskode==null) {
    sikkerhedskode = "" + (int) (Math.random()*100000);
    session.setAttribute("sikkerhedskode", sikkerhedskode);
  }
  // Billedet henter sin tekst fra sessionsattribut "billedtekst", se afsnit 2.8.5
  session.setAttribute("billedtekst", "    Sikkerhedskoden er : "+sikkerhedskode);
%>
<img src="../kapitel_02/billede.jsp">
  <input type="submit" name="handling" value="opret bruger">
</form>

<font color="red">
<%
  String handling = request.getParameter("handling");
  if ("opret bruger".equals(handling)) {
    if (!sikkerhedskode.equals(request.getParameter("sikkerhedskode"))) {
      %>Du har tastet en forkert sikkerhedskode. <% 
    } else   if (login.opretBruger()) { 
      %><jsp:forward page="log_ind.jsp" /><% 
    } else { 
      %>Bruger kunne ikke oprettes. Prøv med et andet brugernavn og tjek epost.<%
    }
  }
%>
<%= login.getMeddelelse() %>
</font>

<br>
<a href="log_ind.jsp">Gå til login</a>
</body>
</html>

9.5.5 En beskyttet side

Den beskyttede side tjekker om der er logget korrekt ind:

beskyttet_side.jsp

<jsp:useBean id="login" class="javabog.Login" scope="session"/>

<%-- klient-omdirigering med JSP --%>
<% if (!login.isLoggetInd()) response.sendRedirect("log_ind.jsp"); %>


<%-- server-omdirigering med JSP
<% if (!login.isLoggetInd()) { %><jsp:forward page="log_ind.jsp"/><% } %>
--%>

<%-- server-omdirigering med JSTL 
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<c:if test="${!login.loggetInd}"><jsp:forward page="log_ind.jsp" /></c:if>
--%>

<html>
<head><title>Beskyttet side</title></head>
<body bgcolor="#ffffff">

<h1>Den beskyttede side</h1>
Denne tekst kan du kun se, hvis du er logget korrekt på.

</body>
</html>

Bemærk at det kunne være en god idé at lægge login-tjekket i et kodefragment, som beskrevet i afsnit 4.2.1. I eksemplet er brugt klient-omdirigering (kommenteret bort findes en server-omdirigering - se afsnit 4.3).

9.5.6 Før eksemplet kan køre på din egen server

Eksemplet bruger klasser fra pakken javax.mail til at sende adgangskoden til brugeren samt MySQL-klasser til at kommunikere med en database.

Disse klasser findes ikke som standard i Tomcat, men de kan nemt installeres, ved at kopiere nogle JAR-filer ind i din webapplikations WEB-INF/lib/ eller Tomcats common/lib/.

For at bruge javax.mail, skal du kopiere filerne mail.jar og activation.jar derind. Søg efter filerne på din computer, de er nemlig sandsynligvis allerede inkluderet i dit udviklingsværktøj. Ellers hent dem fra http://java.sun.com/products/javamail (bemærk at du også skal hente activation.jar fra http://java.sun.com/products/javabeans/glasgow/jaf.html).

Hvordan du får fat i databasedriverklasserne er beskrevet i f.eks. afsnit 5.3.2, Kontakt til MySQL-database. Kopiér også denne JAR-fil ind.

Se afsnit 4.9.6, Hvis klasse(bibliotek)er ikke kan findes, hvis du har problemer med at få webserveren til at finde JAR-filerne.

9.5.7 Opgaver

  1. Afprøv eksemplet, først ved at se det på: http://javabog.dk:8080/JSP/kode/, dernæst i din egen webserver (du skal måske rette i databasedriverklassen og database-URLen).Husk at du skal bruge en række ekstra klasser (se afsnit 9.5.6).

  2. Som koden foreligger, kan enhver registrere sig og logge ind. Du skal derfor ændre (begrænse) systemet, sådan at brugere ikke selv kan registrere sig, men skal være oprettet i forvejen.

  3. Du skal ændre kalender-eksemplet, sådan at brugerne af kalenderen skal være logget ind med brugernavn og adgangskode før de kan få adgang til kalenderen.

  4. Prøv at skrive login-eksemplet om, sådan at brugere godt nok kan registrere sig, men ikke får adgang til de beskyttede sider, før de er blevet godkendt af en administrator.

9.6 Test dig selv

Dette afsnit er ikke omfattet af Åben Dokumentslicens.
Du skal købe bogen for at måtte læse dette afsnit.
Jeg erklærer, at jeg allerede har købt bogen
Jeg lover at anskaffe den i nær fremtid.

9.7 Resumé

Dette afsnit er ikke omfattet af Åben Dokumentslicens.
Du skal købe bogen for at måtte læse dette afsnit.
Jeg erklærer, at jeg allerede har købt bogen
Jeg lover at anskaffe den i nær fremtid.

1Hvis en klasse skal kunne bruges andre steder fra (f.eks. fra en JSP-side) skal den kunne importeres. Det kan klasser uden pakkenavn ikke fra og med JDK 1.4.

2Bruger du MySQL og får fejlen 'Server configuration denies access to data source', så kig på http://dev.mysql.com/doc/connector/j/en/cj-troubleshooting.html for enløsning.

javabog.dk  |  << forrige  |  indhold  |  næste >>  |  programeksempler  |  om bogen
http://javabog.dk/ - Webprogrammering med Java Server Pages af Jacob Nordfalk.
Licens og kopiering under Åben Dokumentlicens (ÅDL) hvor intet andet er nævnt (72% af værket).

Ønsker du at se de sidste 28% af dette værk (275315 tegn) skal du købe bogen. Så får du pæne figurer og layout, stikordsregister og en trykt bog med i købet.