środa, 6 kwietnia 2011

Własne komponenty JSF 2.0

Od dłuższego czasu chodzi mi napisanie biblioteki komponentów JSF zawierającej zestaw flash'owych wykresów FusionCharts Free. Zabierałem się już do tego kilkakrotnie ale zawsze miałem coś pilniejszego do zrobienia. Tym razem jednak zebrałem się w sobie i po lekturze książki, o której pisałem ostatnio postanowiłem przebrnąć przez problematykę tworzenia własnych bibliotek JSF. Początkowo, po wprowadzeniu wersji 2.0, myślałem o wykorzystaniu do tego celu komponentów złożonych (composite components), jednak po głębszym przemyśleniu tematu, doszedłem do wniosku, że bardziej elastyczne będzie utworzenie własnej biblioteki.
Postanowiłem zacząć od czegoś najprostszego – komponentu, który wygeneruje mi HTML'owy element DIV ze wstawionym przez aplikację kliencką tekstem. Komponent taki, może w zasadzie składać się tylko z jednaj klasy – klasy komponentu, dziedziczącej z klasy UIComponent. W praktyce stosuje się głównie dziczenie po jednej z trzech klas: UIOutput, UIInput, UIComand.
Twórcy technologii JSF założyli, że formatem wyjściowym może być nie tyko HTML (generowany domyślnie prze klasę komponentu) dlatego też, wprowadzili możliwość wydelegowania zadania wizualizacji do odrębnego mechanizmu. Ja jednak, aby nie komplikować tematu rendererów, do którego pewnie jeszcze wrócę, skupię się na podstawowym rozwiązaniu. Poniżej klasa mojego komponentu
package kuba.demo.customcomp;

import java.io.IOException;
import javax.faces.component.FacesComponent;
import javax.faces.component.UIOutput;
import javax.faces.context.FacesContext;
import javax.faces.context.ResponseWriter;

@FacesComponent("com.blogspot.javaspotlight.Div")
public class UIDiv extends UIOutput{

   @Override
   public void encodeBegin(FacesContext context) throws IOException {

      ResponseWriter writer = context.getResponseWriter();
      String clientId = getClientId(context);

      String compId   = (String)getAttributes().get("id");
      String divStyle = (String)getAttributes().get("style");
      String divText  = (String)getAttributes().get("divText");

      writer.startElement("div", this);

      if (compId != null){
         writer.writeAttribute("id", compId, "id");
      }else{
         writer.writeAttribute("id", clientId, null);
      }

      if (divStyle != null){
         writer.writeAttribute("style", divStyle, null);
      }

      if (divText != null){
         writer.writeText(divText, null, null);
      }

      writer.endElement("div");
   }
}

Jej zadaniem jest wygenerowanie elementu div z trzema atrybutami pochodzącymi z a aplikacji klienckiej: id, style, divText.
Warto też zwrócić uwagę na adnotację @FacesComponent("com.blogspot.javaspotlight.Div"). Zawiera ona identyfikator klasy komponentu JSF. We wcześniejszych wersjach odwzorowanie identyfikatora na klasę komponentu umieszczało się w pliku faces-config.xml






Od wersji 2.0 można używać obu metod odwzorowań.
Drugim krokiem tworzenia własnego komponentu JSF jest utworzenie w katalogu WEB-INF pliku deskryptora biblioteki zawierającego: przestrzeń nazw, nazwę znacznika i typ komponentu. Jego nazwa musi posiadać zakończenie .taglib.xml
   



























Warto zwrócić uwagę na jedną rzecz – deklarację atrybutów znacznika. Nie jest ona wymagana, klasa komponentu i tak będzie „widziała” atrybuty. Jednak bez tej deklaracji tworząc aplikację z wykorzystaniem własnej biblioteki, nie będzie ich rozpoznawać nasze środowisko programistyczne – w moim przypadku NetBeans.
Wreszcie krok trzeci – ostatni. W pliku web.xml musimy wskazać na położenie deskryptora






Teraz pozostaje już tylko użycie komponentu na stronie.













Teoretycznie to już wszystko, tylko po co nam taki komponent ? Celem bibliotek jest możliwość ich wielokrotnego wykorzystania w różnych projektach. Pozostaje jeszcze spakowanie naszego komponentu. Dopiero tutaj zaczął się problem :)
Przygotowanie komponentu JSF do dystrybucji
  1. utworzyć archiwum .jar
  2. przekopiować deskryptor .taglib.xml do katalogu META-INF
  3. w katalogu META-INF musi znajdować się plik faces-config.xml – nawet jeśli nie zawiera żadnych wpisów (tutaj właśnie utknąłem – przecież JSF 2.0 załatwia wszystko przez adnotacje i teoretycznie ten plik nie jest potrzebny. Jakże się myliłem – JEST POTRZEBNY :))
  4. plik web.xml nie jest potrzebny
Struktura biblioteki po spakowaniu powinna wyglądać następująco

Własny język wyrażeń (Expression Language) w JSF 2.0

Dzisiejszy wpis ma na celu pokazanie na przykładzie dwóch prostych przypadków, jak rozszerzyć istniejący język wyrażeń EL.

Przykład 1 – rozszerzenie języka wyrażeń

Załóżmy, że zaimplementowaliśmy własny mechanizm warunkowego renderowania komponentów JSF (w zależności od posiadanych przez użytkownika ról)
Wyrażenie #{userHasRole['EMPLOYEE, MANAGER']} powinno zwracać true lub false. Wymaga to utworzenia własnego mechanizmu przetwarzającego, który w pierwszym kroku zinterpretuje ciąg znaków userHasRole , a następnie ciąg 'EMPLOYEE, MANAGER'. Mechanizm ten to nic innego jak rozszerzenie klasy ELResolver , której najważniejszą dla nas jest metoda:

public Object getValue(ELContext context, Object base, Object property)

Poniżej kod własnego resolver'a
package kuba.demo.el;

import java.beans.FeatureDescriptor;
import java.util.Iterator;
import javax.el.ELContext;
import javax.el.ELResolver;
import kuba.demo.UserRolesController;

public class UserRolesResolver extends ELResolver{ 

    @Override
    public Object getValue(ELContext context, Object base, Object property) {

        // base=null  property=userHasRole

        if(base==null && "userHasRole".equals(property)){

            context.setPropertyResolved(true);

           return new UserRolesController();

        }

        // base=UserRolesController  property=EMPLOYEE, MANAGER

        if(base instanceof UserRolesController && property instanceof String){

            context.setPropertyResolved(true);

           return UserRolesController.checkUserAccess((String)property);

        }        

        return false;
    }
  

    @Override
    public Class getType(ELContext context, Object base, Object property) {



         if (base instanceof UserRolesController) {

            context.setPropertyResolved(true);

            return UserRolesController.class;

        }

      return null;

    }


    @Override
    public Class getCommonPropertyType(ELContext context, Object base) {

        if (base instanceof UserRolesController) {

            context.setPropertyResolved(true);

            return String.class;

        }

      return null;

    }


    @Override
    public boolean isReadOnly(ELContext context, Object base, Object property) {

        if (base instanceof UserRolesController) {

            context.setPropertyResolved(true);

            return true;
        }
      return false;
    }


    @Override
    public void setValue(ELContext context, Object base, Object property, Object value) {
    }


    @Override
    public Iterator getFeatureDescriptors(ELContext context, Object base) {
       return null;

    }
}

Ja, na potrzeby niniejszego przykładu, dodałem jeszcze klasę pomocniczą UserRolesController przechowującą i sprawdzającą role użytkownika
package kuba.demo;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.StringTokenizer;

public class UserRolesController {

    public static boolean checkUserAccess(String roleNames) {

        List userRoles = new ArrayList(Arrays.asList("ADMIN",
                                                     "MANAGER",
                                                     "USER"
                                                    ));

        StringTokenizer token = new StringTokenizer(roleNames,",");
      
        while (token.hasMoreTokens()) {

            String roleName = ((String)token.nextElement()).trim();

            if(userRoles.contains(roleName)){

                return true;

            }
        }
        return false;
    }
}

Aby nasza klasa przetwarzająca EL była widoczna w aplikacji, musimy dodać następujący w pis w pliku faces-config.xml





Jest tutaj pewna niekonsekwencja twórców specyfikacji. Nie istnieje bowiem możliwość rejestracji własnego resolver'a przy pomocy odpowiedniej adnotacji – tak jak odbywa się to w przypadku innych elementów JSF 2.0.  

Przykład 2 – dodanie funkcji do języka wyrażeń

Załóżmy, że chcemy na tronie JSF wywołać przy pomocy EL własną funkcję przyjmującą dwa argumenty



W tym celu musimy zaimplementować statyczną metodę
package kuba.demo.el;

public class SumELFunction {

    public static int sumTwoArgs(int arg1, int arg2){
        return arg1 + arg2;
    }
}

oraz odwzorować ją w znajdującej się w katalogu WEB-INF bibliotece znaczników el.taglib.xml
















pozostaje jeszcze zarejestrować bibliotekę w pliku web.xml


Java Context and Dependency Injection w akcji

Dzisiaj niechcący wygooglowałem projekt poświęcony w całości framewokowi Context and Dependency Injection – CDISource. Muszę przyznać, że pod względem treści wygląda bardzo imponująco. Jak twierdzą autorzy, Andy Gibson i Rick Hightower powstał on w celu promocji CDI w kontekście różnych możliwych zastosowań – nie tylko w Java EE. Myślę, że warto śledzić rozwój tego projektu, tym bardziej, że zawiera on nie tylko opisy rozwiązań, ale także spore ilości przykładowego kodu.

P.S.
Wiem. Od bardzo dawna nic ciekawego nie napisałem. Niestety cierpię na permanentny brak czasu. Szczególne wyrzuty sumienia mam wobec Grześka Kukawskiego - autora Darmowego kursu UML, na który się zapisałem i niestety utknąłem na drugiej części. Na swoje usprawiedliwienie mam jedynie to, że nie przebimbałem tego czasu. W ostatnim czasie nasza firmowa biblioteka wzbogaciła się o kolejną pozycję – trzecie wydanie JavaServer Faces (David Geary, Cay S. Horstmann) – nie mogłem sobie odpuścić i jej przeczytanie stanęło na pierwszym miejscu :)

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)

środa, 19 stycznia 2011

Inastalacja Informix'a 11.70 na Ubuntu 10.04

Jakieś 3 tygodnie temu ostatecznie rozstałem się z Windowsem i dotychczas nie zainstalowałem żadnej lokalnej bazy.  Biorąc pod uwagę, że ostatnio ostro wziąłem się za naukę Hibernate, a w swoim Redbook'u pod tytułem IBM Informix Developer's Handbook producent zaleca używanie właśnie jego – postanowiłem upiec dwie pieczenie na jednym ogniu - czyli rozpoznać dobrze i jedno i drugie.
Zdecydowałem się na najnowszą, trialową wersję Ultimate Edition  11.70 (dostępne są 2 darmowe, ale okrojone wersje – Developer Edition i Innovator-C Edition) ponieważ zawiera ona wszystkie dostępne cechy bazy w wersji Enterprise.

Instalację przeprowadziłem w trybie graficznym wydając polecenie: ./ids_install -i gui


W trakcie instalacji utworzony został sytemowy użytkownik informix i tylko on może uruchomić bazę. Aby to zrobić wymagane jest ustawienie kilku zmiennych środowiskowych. Ja utworzyłem w tym celu w jego katalogu domowym plik profilu /home/informix/.profile z następującymi zmiennymi:

INFORMIXDIR=/opt/IBM/informix
INFORMIXSERVER=ol_informix1170
ONCONFIG=onconfig.ol_informix1170
INFORMIXSQLHOSTS=/opt/IBM/informix/etc/sqlhosts.ol_informix1170
PATH=${PATH}:${INFORMIXDIR}/bin:${INFORMIXDIR}/extend/krakatoa/jre/bin
export INFORMIXDIR INFORMIXSERVER ONCONFIG INFORMIXSQLHOSTS PATH


To w zasadzie wszystko. Można teraz poleceniem oninit uruchomić serwer bazy danych

Status serwera możemy sprawdzić poleceniem onstat

 Serwer zatrzymujemy poleceniem onmode -ky

Informix zawiera demonstracyjną bazę danych o nazwie stores_demo. Aby ją zainstalować wydajemy polecenie dbaccessdemo7.

Do pracy z bazą najlepiej nadaje się chyba darmowy oparty o środowisko Eclipse IBM Data Studio Standalone. Do pobrania jest także darmowy podręcznik użytkownika Getting started with IBM Data Studio for DB2 . Poniżej proces instalacji i konfiguracji.

piątek, 14 stycznia 2011

Lektura na weekend

Przymierzałem się do tego od jakiegoś czasu i skuszony pozytywną recenzją Jacka Laskowskiego zakupiłem dzisiaj książkę (podobno lektura obowiązkowa dla każdego programisty Javy) "Hibernate. Od nowicjusza do profesjonalisty" wydaną nakładem wydawnictwa Power Net (oryg. Apress). Po pobieżnym przejrzeniu mogę powiedzieć - warto było. Jedynym chyba minusem jest fakt, że nie jest to II wydanie tej pozycji, które ukazało się w ubiegłym roku. Niestety jak dowiedziałem się w wydawnictwie nie planują oni upgrade'u. Szkoda, że dzisiaj już nie wezmę się za lekturę. Jutro egzamin na 7 Kup - trzeba jeszcze powtórzyć układy i teorię :)

sobota, 8 stycznia 2011

Cyfrowe podpisywanie plików PDF

Dzisiejszy post trochę odbiega od poprzednich, niemniej jednak uznałem, że temat jest warty uwagi. W tym tygodniu zajmowałem się podpisywaniem plików PDF przy pomocy klucza prywatnego i weryfikacją takiego podpisu w oparciu o klucz publiczny.

Zacznijmy od wygenerowania repozytorium z parą takich kluczy przy pomocy keytool'a

keytool -genkey -alias kuba  -keypass demokeypass  -storepass demostorepass  -keystore demo-keystore.jks -validity 360 -dname "CN=Jakub Pawlowski, OU=blog, O=javaspotlight.blogspot.com, L=Lodz, S=Lodzkie, C=PL"

Następnie należy wyeksportować klucz publiczny
keytool -export -alias kuba  -keypass demokeypass  -storepass demostorepass  -keystore demo-keystore.jks -file demo-cert.cer

Poniższe polecenia umożliwiają weryfikacje zawartości obu plików
keytool -list -alias kuba -storepass demostorepass -v -keystore ~/tmp/demo-keystore.jks

keytool -printcert -v -file ~/tmp/demo-cert.cer

Do podpisania PDF'a użyłem oczywiście biblioteki iText (wymagana jest także biblioteka Bouncy Castle).
Klasa odpowiedzialna za podpisanie PDF'a wygląda następująco
import com.itextpdf.text.DocumentException;
import com.itextpdf.text.pdf.PdfReader;
import com.itextpdf.text.pdf.PdfSignatureAppearance;
import com.itextpdf.text.pdf.PdfStamper;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.UnrecoverableEntryException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.util.logging.Logger;

public class PdfSigner {

    private static final Logger log = Logger.getLogger(PdfSigner.class.getName());
    private static final String PDF_PATH = "/home/kuba/tmp/demo-doc.pdf";
    private static final String PDF_PATH_SIGNED = "/home/kuba/tmp/demo-doc-signed.pdf";
    private static final String KEYSTORE_PATH = "/home/kuba/tmp/demo-keystore.jks";
    private static final String KEYSTORE_PASSWORD = "demostorepass";
    private static final String KEY_ALIAS = "kuba";
    private static final String KEY_PASSWORD = "demokeypass";

    private Certificate[] certificates;
    PrivateKey privateKey;

    public PdfSigner() {

        try {
             KeyStore ks = KeyStore.getInstance("JKS");
             FileInputStream fis = new FileInputStream(KEYSTORE_PATH);
             ks.load(fis, KEYSTORE_PASSWORD.toCharArray());
             certificates = ks.getCertificateChain(KEY_ALIAS);
             privateKey = (PrivateKey)ks.getKey(KEY_ALIAS, KEY_PASSWORD.toCharArray());

        } catch (FileNotFoundException ex) {
            log.severe(ex.getMessage());
        } catch (KeyStoreException ex) {
            log.severe(ex.getMessage());
        } catch (IOException ex) {
            log.severe(ex.getMessage());
        } catch (NoSuchAlgorithmException ex) {
            log.severe(ex.getMessage());
        } catch (CertificateException ex) {
           log.severe(ex.getMessage());
        } catch (UnrecoverableEntryException ex) {
          log.severe(ex.getMessage());
        }

    }


    public void signPdf(){

        try {

             PdfReader pdfReader = new PdfReader(PDF_PATH);
             FileOutputStream fos = new FileOutputStream(PDF_PATH_SIGNED);
             PdfStamper stamper = PdfStamper.createSignature(pdfReader, fos, '\0');
             PdfSignatureAppearance appearance = stamper.getSignatureAppearance();
             appearance.setReason("Demo Blog");
             appearance.setCrypto(privateKey, certificates, null, PdfSignatureAppearance.WINCER_SIGNED);
             appearance.setCertificationLevel(PdfSignatureAppearance.CERTIFIED_NO_CHANGES_ALLOWED);
             stamper.close();
             log.info("Document signed");

        } catch (IOException ex) {
           log.severe(ex.getMessage());
        } catch (DocumentException ex) {
           log.severe(ex.getMessage());
        }

    }

}

Po zaimportowaniu klucza publicznego do Adobe Reader'a i otwarciu podpisanego dokumentu, powinny pokazać się następujące informacje o certyfikacie


Poprawność podpisu można także sprawdzić programowo
import com.itextpdf.text.pdf.AcroFields;
import com.itextpdf.text.pdf.PdfPKCS7;
import com.itextpdf.text.pdf.PdfReader;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.SignatureException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.logging.Logger;

public class PdfValidator {

    private static final Logger log = Logger.getLogger(PdfValidator.class.getName());
    private static final String PDF_PATH_SIGNED = "/home/kuba/tmp/demo-doc-signed.pdf";
    private static final String CERTIFICATE_PATH = "/home/kuba/tmp/demo-cert.cer";

    private KeyStore keyStore;

    public PdfValidator() {

        try {
             keyStore = KeyStore.getInstance("JKS");
             keyStore.load(null, null);
             FileInputStream fis = new FileInputStream(CERTIFICATE_PATH);
             CertificateFactory cf = CertificateFactory.getInstance("X509");
             X509Certificate cert = (X509Certificate) cf.generateCertificate(fis);
             keyStore.setCertificateEntry("cacert", cert);

        } catch (FileNotFoundException ex) {
            log.severe(ex.getMessage());
        } catch (KeyStoreException ex) {
            log.severe(ex.getMessage());
        } catch (IOException ex) {
            log.severe(ex.getMessage());
        } catch (NoSuchAlgorithmException ex) {
            log.severe(ex.getMessage());
        } catch (CertificateException ex) {
           log.severe(ex.getMessage());
        }
    }
    

    public void validatePdf(){

        try {
            
            PdfReader reader = new PdfReader(PDF_PATH_SIGNED);
            AcroFields af = reader.getAcroFields();
            ArrayList names = af.getSignatureNames();
            
            for (String name : names) {
                
                PdfPKCS7 pk = af.verifySignature(name);
                Calendar calendar = pk.getSignDate();
                Certificate[] certificates = pk.getCertificates();
                log.info("Revision modified: " + !pk.verify());
                Object[] fails = PdfPKCS7.verifyCertificates(certificates, keyStore, null, calendar);
                if (fails == null) {
                    log.info("Certificates verified against the KeyStore");
                } else {
                    log.info("Certificate failed: " + fails[1]);
                }
            }

        } catch (SignatureException ex) {
            log.severe(ex.getMessage());
        } catch (IOException ex) {
            log.severe(ex.getMessage());
        }
    }
}