niedziela, 20 lutego 2011

Autentykacja zarządzana przez kontener Java EE dla aplikacji JSF 2.0 z wykorzystaniem Servlet 3.0 API

Do napisania niniejszego postu skłoniło mnie spostrzeżenie, że większość śledzących nowości w Java EE 6 (włącznie ze mną oczywiście) skupiła się na adnotacjach. Tymczasem w Servlet API 3.0 weszło bardzo dobre rozwiązanie - a mianowicie - możliwość programowej autentykacji do kontenera JEE. Dotychczas stroną logowania mogła być jedynie zwykła strona HTML z polami j_username, j_password i formularzem z akcją j_security_check. Dzięki wprowadzonym nowościom, strona logowania może być już teraz stroną JSF. Ale zacznijmy od początku. Poniżej kroki konfiguracji GlassFish'a:




















Utworzenie nowego realm'a

Dodanie użytkowników i ról
Struktura mojej aplikacji demo
Plik sun-web.xml
Plik web.xml
Strona logowania
Managed bean z funkcją odpowiedzialną za autentykację

import javax.faces.application.FacesMessage;;
import javax.faces.bean.ManagedBean;
import javax.faces.bean.RequestScoped;
import javax.faces.context.ExternalContext;
import javax.faces.context.FacesContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;

@ManagedBean
@RequestScoped
public class LoginBean {

    private String inputLogin;
    private String inputPassword;

    public String loginAction(){

        FacesContext ctx = null;
        ExternalContext ectx = null;
        HttpServletRequest request = null;
        FacesMessage msg = null;
        String action = null;

        try {
                ctx = FacesContext.getCurrentInstance();
                ectx = ctx.getExternalContext();
                request = (HttpServletRequest) ectx.getRequest();
                request.login(inputLogin, inputPassword);
                action = "/protected/welcome.xhtml";

        } catch (ServletException ex) {

            ctx = FacesContext.getCurrentInstance();
            msg = new FacesMessage(ex.getMessage());
            msg.setSeverity(FacesMessage.SEVERITY_ERROR);
            ctx.addMessage(null, msg);

        }

       return action;
    }

    // getters and setters
    
}

Jak widać nowością jest funkcja login() wywoływana na obiekcie HttpServletRequest. W przypadku niepoprawnego logowania otrzymamy w wyjątek, który można obsłużyć dowolnym faces'owym komunikatem.









Analogicznie, możemy wywołać funkcję logout(), przy czym należy pamiętać, że wylogowanie musi się odbywać z redirect'em
@ManagedBean
@RequestScoped
public class WelcomeBean {


    public String logoutAction(){

        FacesContext ctx = null;
        ExternalContext ectx = null;
        HttpServletRequest request = null;
        String action = null;

        try {
                ctx = FacesContext.getCurrentInstance();
                ectx = ctx.getExternalContext();
                request = (HttpServletRequest)ectx.getRequest();
                request.logout();
                action = "/faces/index.xhtml?faces-redirect=true";

        } catch (ServletException ex) {
            ex.printStackTrace();
        }

      return action;
    }
}

Pobierz projekt (NetBeans 6.9.1)

Kto nie ma w głowie - ten klepie zbędny kod

Tytuł jak najbardziej adekwatny do tematu dzisiejszego postu. Niestety powyższa (nomen omen) refleksja naszła mnie dopiero pod koniec projektu - sporej wielkości aplikacji do raportowania z bazy, czyli mapowanie ResultSet'ów na kolekcje klas POJO. Wszystko niby fajnie ale wyniki wyświetlane na stronie trzeba było przesłać do Birt Viewer'a, co skutkowało pisaniem dodatkowej klasy handlera dla każdej tabelki – a można było prościej. W poniższym przykładzie wykorzystam własne adnotacje atrybutów POJO do opisania wyglądu raportu PDF (dla uproszczenia użyłem biblioteki iText).

Mając poniższą klasę POJO
package kuba.demo.dao;

public class Employee {

    private String firstName;
    private String lastName;
    private double salary;

    public Employee() {
    }

    public Employee(String firstName, String lastName, double salary) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.salary = salary;
    }

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    public double getSalary() {
        return salary;
    }

    public void setSalary(double salary) {
        this.salary = salary;
    }
}

oraz managed beana
package kuba.demo.view;

import java.util.List;
import javax.annotation.PostConstruct;
import javax.faces.bean.ManagedBean;
import javax.faces.bean.RequestScoped;
import javax.faces.event.ActionEvent;
import kuba.demo.dao.Employee;
import kuba.demo.dao.EmployeesDAO;

@ManagedBean(name = "employeeBean")
@RequestScoped
public class EmployeeBean {

    private List emp;
    
    
    @PostConstruct
    private void init(){

        emp = EmployeesDAO.getAll();
    }


    public List getEmp() {
        return emp;
    }

    public void setEmp(List emp) {
        this.emp = emp;
    }
}

wyświetlam na stronie prostą tablekę JSF

Chciałbym ją wyeksportować do pliku PDF co oczywiście nie jest problemem, tyle  że przy kilkuset tabelach dla każdego eksportu musiałbym pisać osobną klasę opisującą formatowanie tej tabeli.
Mój cel, to sprowadzić kod odpowiedzialny za generowanie PDF do takiej postaci
public void downloadPdf(ActionEvent evt){
 
        PdfBuilder builder = new PdfBuilder(emp);
        builder.printToOutputStream("TestWeb");
    }

Tabelki są jedna różne – gdzie więc kod odpowiedzialny za takie informacje jak nagłówek czy szerokość kolumny? Tu z pomocą przychodzą własne adnotacje.
Moja wyglądają następująco
public class Employee {

    @Pdf(label="Imie",width=100)
    private String firstName;

    @Pdf(label="Nazwisko",width=150)
    private String lastName;

    @Pdf(label="Pensja",width=50)
    private double salary;

    // getters setters
}

Drugim krokiem jest utworzenie typu adnotacji
package kuba.demo.pdf;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Pdf {

    String label() default "";
    int    width() default 20;
}

I to w zasadzie wszystko. Ja musiałem utworzyć jeszcze pomocniczą klasę przechowującą na potrzeby generowania PDF informacje o atrybutach tabeli wczytanych z argumentów adnotacji

package kuba.demo.pdf;

public class PdfTableModel {

    private String fieldName;
    private String label;
    private int width;
    
    // getters setters
}

I wreszcie klasa generująca PDF
package kuba.demo.pdf;

import com.itextpdf.text.Document;
import com.itextpdf.text.DocumentException;
import com.itextpdf.text.Phrase;
import com.itextpdf.text.pdf.PdfPCell;
import com.itextpdf.text.pdf.PdfPTable;
import com.itextpdf.text.pdf.PdfWriter;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Logger;
import javax.faces.context.ExternalContext;
import javax.faces.context.FacesContext;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;

public class PdfBuilder {

    private static final Logger log = Logger.getLogger(PdfBuilder.class.getName());

    private List data;
    private Class listElement;
    private List tableModel;

    private Document document;

    public PdfBuilder(List data) {
        this.data = data;
    }


    private void scanAnnotations(){

        if(data != null && data.size()>0){
            listElement = data.get(0).getClass();
        }

        tableModel = new ArrayList();

        for(Field field : listElement.getDeclaredFields()){

            Pdf pdf = field.getAnnotation(Pdf.class);

            if(pdf != null){

                String fieldName = field.getName();
                String label = pdf.label();
                int width = pdf.width();

                tableModel.add(new PdfTableModel(fieldName,label,width));
            }
        }
    };


    private void createDocument(Document document){

        scanAnnotations();

        try {

            Object row = Class.forName(listElement.getName()).newInstance();

            int tableSize = tableModel.size();
            float[] columns = new float[tableSize];
            
            int i = 0;
            for(PdfTableModel ptm: tableModel){
                columns[i] = ptm.getWidth();
                i++;
            }

            PdfPTable table = new PdfPTable(columns);

            // Table headers
            for(PdfTableModel ptm : tableModel){

                PdfPCell cell = new PdfPCell(new Phrase(ptm.getLabel()));
                cell.setHorizontalAlignment(PdfPCell.ALIGN_CENTER);
                table.addCell(cell);
            }
            
            // Table rows
            for(Object listRow : data){

                row = listRow;

                for(PdfTableModel ptm : tableModel){

                    Field f = row.getClass().getDeclaredField(ptm.getFieldName());
                    f.setAccessible(true);
                    Object cellValue = f.get(row);
                    PdfPCell cell = new PdfPCell(new Phrase(cellValue.toString()));
                    table.addCell(cell);
                 }
            }

            document.add(table);

        } catch (InstantiationException ex) {
            log.severe(ex.getMessage());
        } catch (ClassNotFoundException ex) {
            log.severe(ex.getMessage());
        } catch (IllegalArgumentException ex) {
            log.severe(ex.getMessage());
        } catch (IllegalAccessException ex) {
            log.severe(ex.getMessage());
        } catch (NoSuchFieldException ex) {
            log.severe(ex.getMessage());
        } catch (SecurityException ex) {
            log.severe(ex.getMessage());
        } catch (DocumentException ex) {
            log.severe(ex.getMessage());
        }

    }

    
    public void printToOutputStream(String fileName){

        ServletOutputStream out = null;

        FacesContext ctxContext = FacesContext.getCurrentInstance();
        ExternalContext ectx = ctxContext.getExternalContext();
        HttpServletResponse res = (HttpServletResponse)ectx.getResponse();
        res.setContentType("application/pdf");
        res.setHeader("Content-Disposition", " inline; filename=\""+fileName+".pdf\"");

        try {

             out = res.getOutputStream();
             document = new Document();
             PdfWriter.getInstance(document, out);
             document.open();
             createDocument(document);
             document.close();
             
        } catch (IOException ex) {
            log.severe(ex.getMessage());
        }catch (DocumentException ex) {
            log.severe(ex.getMessage());
        }
    }


    public void printToFile(String filePath){

        try {
                document = new Document();
                PdfWriter.getInstance(document, new FileOutputStream(filePath));
                document.open();
                createDocument(document);
                document.close();
                
        }catch (FileNotFoundException ex) {
            log.severe(ex.getMessage());
        }catch (DocumentException ex) {
            log.severe(ex.getMessage());
        }
    }
}

scanAnnotations() - odczytuje wartości argumentów adnotacji i zapisuje je do listy obiektów PdfTableModel (nagłówki i szerokości kolumn)
createDocument() - buduje tabelę w dokumencie PDF wykorzystując refleksje do odczytania wartości atrybutów POJO
printToOutputStream() - zapisuje dokument PDF do obiektu ServletOutputStream

Jak widać działa :)
Pobierz projekt (NetBeans 6.9.1)