středa 28. listopadu 2007

Jak na Excel 4 - JDBC driver

Když už jsem v úvodu seriálu zmínil možnost přístupu k Excel souborům přes JDBC, měl bych teď přidat více detailů. Ale nečekejte žádné zázraky. Osobně jsem tímto způsobem s excelem nikdy nepracoval a ani samotná myšlenka se mi moc nelíbí, nicméně je to také jedna z cest. Narazil jsem pouze na dvě knihovny, které poskytovaly JDBC rozhraní pro Excel:

HXTT JDBC driver jsem nezkoumal a xlSQL mě zklamal - chybí dokumentace, vývoj zamrzl v roce 2004 a příklady použití aby člověk hledal "kupou v lupce sena" :-).

Pro zpracování xls dokumentů používá xlSQL knihovnu projektu JExcelApi, ke které se snad dostaneme v některém z příštích dílů. Zpracování SQL dotazů probíhá v režii knihoven z MySQL connectoru a HSQLDB.

A abych zůstal věrný tradici a netrápil vás pouze šedou teorií, tak tady je příklad kódu jak na excel přes JDBC:

Class.forName("com.nilostep.xlsql.jdbc.xlDriver");
String url = "jdbc:nilostep:excel:" + System.getProperty("user.dir");
Connection con = DriverManager.getConnection(url);
Statement stm = con.createStatement();

String sql = "DROP TABLE \"demo.xlsqly7\" IF EXISTS;"
             + "CREATE TABLE \"demo.xlsqly7\" (v varchar);";
stm.execute(sql);

// some inserts
for (int i = 0; i < 7000; i++) {
    sql = "INSERT INTO \"demo.xlsqly7\" VALUES ('xlSQL Y7 - NiLOSTEP');";
    stm.execute(sql);
}
//query count
sql = "select count(*) from \"demo.xlsqly7\"";
ResultSet rs = stm.executeQuery(sql);

//print resutl
while (rs.next()) {
    System.out.println("Sheet xlsqly7 has " + rs.getString(1)
                       + " rows.");
}

// close connection
con.close();

úterý 20. listopadu 2007

Hibernate: Řekněte Ano místo Yes

V Hibernate existuje yes_no typ, který vám umožňuje mapovat B(b)oolean property na databázové sloupce typu CHAR(1). Pro pravdivostní hodnotu true je při reprezentaci v databázi použito hodnoty 'Y' a pro false 'N'. Občas se ale setkáte s databází jejíž tvar nemůžete ovlivnit a pro pravdivostní hodnoty jsou použity jiné zástupné znaky např. 'A', 'N' (ano/ne) nebo 'J', 'N' (ja/nein). Pro některé typy úkolů sice postačuje modifikovat nastavení hibernate, aby v dotazech na yes_no hodnotu nahradil příslušné znaky:

<prop key="hibernate.query.substitutions">yes 'A', no 'N'</prop>

Tento přístup si ale už neporadí s nahráváním celých objektů z databáze - také hodnoty 'A' budou konvertovány na false. V tomto případě se hodí vytvořit si nový typ a můžeme využít toho, co pro nás už Hibernate připravil. Rozšíříme abstraktní třídu CharBooleanType:

package cz.mujpackage;

import org.hibernate.type.CharBooleanType;

/**
* Ano/Ne typ pro reprezentaci booleanu v databazi pomoci znaku
* 'A' a 'N'
* @author Josef Cacek
*/
public class AnoNeType extends CharBooleanType {

  private static final long serialVersionUID = 1L;

  /**
   * @see org.hibernate.type.CharBooleanType#getTrueString()
   */
  protected final String getTrueString() {
    return "A";
  }

  /**
   * @see org.hibernate.type.CharBooleanType#getFalseString()
   */
  protected final String getFalseString() {
    return "N";
  }

  public String getName() { return "ano_ne"; }

}

Teď už jenom říct Hibernatu, že chceme tento typ použít pro náš sloupec

<property name="sloupec" type="cz.mujpackage.AnoNeType" column="SLOUPEC"/>

a v Java beaně si vytvořit odpovídající B(b)oolean property:

private Boolean sloupec;

public Boolean getSloupec() {
  return sloupec;
}

public void setSloupec(Boolean sloupec) {
  this.sloupec = sloupec;
}

Jsme hotovi a všechno krásně funguje. Hibernatu třikrát hurá.

pátek 16. listopadu 2007

A všichni si rozumíme - budiž Resource Bundle

Problém s aplikacemi pro různé jazyky je, že musíte udržovat překlady konzistentní. Já jsem si své property fajly zatím spravoval ručně jen editací v Eclipsu, ale tenhle stav už je celkem neudržitelnej, takže se poohlížím po vhodném nástroji, který by se staral za mě. Při svém hledání jsem narazil na vhodný odrazový můstek a tím je tato stránka:

Zatím se mi nejvíc líbí Babel Fish, ale jestli víte ještě o něčem lepším, tak napište do komentářů.

Jak na Excel 3 - DCOM

V třetím díle seriálu o práci s Excelem v Javě si popíšeme přístup přes DCOM bez využití JNI. Tento způsob práce se hodí v případě, kdy potřebujete úplnou funkcionalitu Excelu, ale vaše aplikace neběží na počítači kde by byl Excel nainstalovaný.

Přístup je velice obdobný tomu, co jsme si ukázali v druhém díle seriálu — tedy využití COM přes JNI. Opět existují implementace pomocí zástupných objektů (komerční J-Integra, WebLogic jCOM) i pomocí volání IDispatch (opensource J-Interop).

Výhoda přístupu přes DCOM spočívá v tom, že vaše java aplikace nemusí běžet na windows a nevyužívá nativní knihovny (je zachována platformová nezávislost). Na druhou stranu vám díky síťové komunikaci může výrazně klesnout výkon. DCOM přístup je také náročnější na nastavení na straně windows serveru (vzálený přístup k DCOM nebývá ve výchozím nastavení povolený). Další problém nastane se sdílením souborů, když chcete číst nebo zapisovat XLS soubor windows server na něj musí vidět (cesty k souboru jsou platné na straně serveru). Zajímavé řešení nabízí BEA v jCOM modulu pro WebLogic - můžete si sami zvolit zda použijete nativní volání COM (pouze na windows) nebo zda se použije síťový DCOM bez JNI.

A jak by vypadal náš příklad z minulého dílu, kdybychom ho chtěli spouštět pomocí J-Interop:

IJIDispatch dispatchApp = null;
JISession session = null;
try {
  //vytvoreni session a napojeni na COM server
  session = JISession.createSession("windows.domena.cz","uzivateldcom","heslo@uzivateldcom");
  JIComServer comServer = new JIComServer(JIProgId.valueOf(session,"Excel.Application"),"servername.cz",session);
  //vytvoreni instance Excelu
  IJIComObject unknown = comServer.createInstance();
  dispatchApp = (IJIDispatch)ComFactory.createCOMInstance(ComFactory.IID_IDispatch,unknown);
  IJITypeInfo typeInfo = dispatchApp.getTypeInfo(0);
  typeInfo.getFuncDesc(0);
  //ziskani kolekce workbooku
  int dispId = dispatchApp.getIDsOfNames("Workbooks");
  JIVariant outVal = dispatchApp.get(dispId);
  JIInterfacePointer ptr = (JIInterfacePointer)outVal.getObject();
  IJIDispatch dispatchOfWorkBooks =(IJIDispatch)ComFactory.createCOMInstance(unknown,ptr);
  //Otevreni workbooku ze souboru
  JIVariant[] outVal2 = dispatchOfWorkBooks.callMethodA(
      "Open",
      new Object[] {new JIString("c:\\temp\\test.xls")},
      new String[] {"Filename"});
  ptr = (JIInterfacePointer)outVal2[0].getObject();
  IJIDispatch dispatchOfWorkBook =(IJIDispatch)ComFactory.createCOMInstance(unknown,ptr);
  //spusteni makra
  outVal2 = dispatchApp.callMethodA(
      "Run",
      new Object[] { new JIString("JakNaExcel2")},
      new String[] {"Macro"});
  System.out.println(outVal2[0].getObjectAsString().getString());
} catch (Exception e) {
    System.err.println(e.getMessage());
  e.printStackTrace();
} finally {
  //Pozor aby nam pri chybe nezustaval viset Excel
  //ukonceni excelu
  try {
    dispatchApp.callMethod("Quit");
  } catch (Exception ex) {
    ex.printStackTrace();
  }
  //uzavreni session
  try {
    JISession.destroySession(session);
  } catch (Exception ex) {
    ex.printStackTrace();
  }
}

Další knihovny podporující COM/DCOM komunikaci v javě můžete najít v těchto seznamech:

Při testování J-Interop jsem narazil na chybku. Jestliže uživatelské heslo obsahuje mezeru, přihlášení se nepovede a vyskočí vám JIException. Problém jsem zahlásil do chybové databáze projektu.

pondělí 12. listopadu 2007

Jak na Excel 2 - Automation a JNI

První metoda pro práci s Excelem v Javě, kterou si v seriálu ukážeme je využití MS Automation. To znamená že budeme s Excelem pracovat stejným způsobem jako při psaní skriptů ve windows (viz Windows Scripting). To sice přináší největší funkcionalitu, ale na druhé straně spoustu omezení spočívající v předpokladech, které musí aplikace splnit. Tuto metodu doporučuji pouze v případě, že chcete používat funkcionalitu, které se nedá docílit použitím jiných metod (viz Jak na Excel 1) - například spouštění maker.

Co budeme muset splnit pro využití této metody?

  • naše Java aplikace musí běžet na stroji s Windows, kde musí být nainstalovaný Excel
  • musíme mít Java-COM bridge, t.j. nástroj který nám umožní v Javě pracovat s COM objekty, většinou nějakou knihovnu volající přes JNI funkce z MS Windows

Existuje několik open source knihoven implementujících Java-COM bridge. Tyto knihovny pracují v některém ze dvou základních režimů (některé zvládají oba). První typ přístupu využívá vygenerovaných zástupných objektů (tzv stub objekty), které reprezentují jednotlivé interfacy COM objektů. Druhý přístup nazývaný scriptable COM pracuje přes OLE Automation rozhraní IDispatch a je podobný Java reflexi (identifikátor volané metody je pak prvním parametrem funkce invoke).

Přístup přes stub objekty má výhodu, že máme zaručenu korektnost už v době kompilace programu. Ale zase musíme hledat windows knihovny, ve kterých je definice daných typů a potom z nich generovat zmíněné zástupné objekty.

Přístup přes rozhraní IDispatch je univerzálnější a hodí se například v případě, kdy pracujete s více různými komponentami, nebo s různými verzemi MS produktů (pamatuji, jak jsme kdysi řešili problém, kdy naše verze Outlooku měla jiné API než zákaznikova).

A teď už začneme programovat reálný příklad. Budeme chtít otevřít Excel dokument a spustit makro, které nám posléze vrátí nějaký výsledek. Nejprve si připravíme xls soubor - spustíme excel, otevřeme editor VBA (Alt+F11), vložíme modul do projektu a vepíšeme kód:

Function JakNaExcel2()
    JakNaExcel2 = "Na excel jednoduše"
End Function

A nyní zajímavější část, tedy Java. V následujícím seznamu naleznete open source knihovny implementující Java-COM bridge:

Nejdříve si ukážeme jak pracovat se stub objekty a využijeme k tomu knihovnu com4j. V prvním kroku musíme zjistit, kde se nachází definice typů COM objektů (dll, exe, ocx, ...). Pro Excel 2003, který momentálně používám jsou typy definovány přímo v souboru excel.exe. Nyní už můžeme použít generátor zástupných typů pro Javu z projektu com4j:

>java -jar tlbimp.jar -o excel -p cz.cacek.javlog.excel "c:\Program Files\Microsoft Office\OFFICE11\EXCEL.EXE"

Tím se nám vygenerovaly zdrojové kódy a nyní už můžeme vesele Javit:

_Application app = ClassFactory.createApplication();
Workbooks workbooks = app.workbooks();
_Workbook wb = workbooks.open("c:\\temp\\test.xls", 
    null, null, null, null, null, null, null, null, null,
    null, null,null, null, null, 0);
System.out.println(app.run("JakNaExcel2",
    null, null, null, null, null, null, null, null, null, null,
    null, null, null, null, null, null, null, null, null, null,
    null, null, null, null, null, null, null, null, null, null
    ));
wb.save(0);
app.quit();

Práce se zástupnými objekty je sice ve vývojových prostředích velice pohodlná, ale jak je vidět z příkladu, problém může nastat při velkém počtu volitelných argumentů.

Pro demonstraci varianty využívající rozhraní IDispatch použijeme projekt Jawin.

try {
  Ole32.CoInitialize();
  DispatchPtr app = new DispatchPtr("Excel.Application");
  DispatchPtr workbooks = (DispatchPtr) app.get("Workbooks");
  DispatchPtr workbook = (DispatchPtr) workbooks.invoke("Open", "c:\\temp\\test.xls");
  System.out.println(app.invoke("Run","JakNaExcel2"));
  workbook.invoke("Save");
  app.invoke("Quit");
  Ole32.CoUninitialize();
} catch (Exception e) {
  e.printStackTrace();
}

Nejste-li zběhlí v Excel API doporučuji vám stisknout ve VBA Editoru klávesu F2, čímž si otevřete okno objektového katalogu a můžete vesele studovat objektový model. :-)

pátek 9. listopadu 2007

Jak na Excel 1 - úvod

Už se mi několikrát stalo, že v zákaznických požadavcích na aplikaci byl i export tabulek do Excelu nebo naopak import dat z .xls souborů, proto bych chtěl napsat pár záznamů do blogu i na toto téma.

Existuje několik různých přístupů jak v Javě pracovat s dokumenty Excelu, každý má svá pro i proti. Zde je stručný přehled:

přístupvýhodynevýhody
JNI a Excel Automation (COM)
  • jednoduše implementovatelné
  • "úplná" funkcionalita (v rámci toho co Excel nabízí přes komponentový model)
  • nehrozí nekompatibilita (když nepočítám některé nekompatibility mezi jednotlivými verzemi Excelu)
  • musí být nainstalovaný Excel
  • přicházíme o platformní nezávislost (JNI)
  • většina řešení pouze pro windows
Java DCOM + Excel Automation
  • nehrozí nekompatibilita (když nepočítám některé nekompatibility mezi jednotlivými verzemi Excelu)
  • nevyužívá JNI
  • musí být povolen DCOM přístup na počítač s nainstalovaným Excelem
  • větší časová náročnost (síťová komunikace)
  • nezanedbatelné nároky na správu sítě (povolení DCOM přístupu, sdílení souborů mezi serverem a klientem, ...)
Nástroje třetích stran (např. OpenOffice.org SDK)
  • vysoká kompatibilita s Excelem
  • velká funkcionalita (např. podpora mnoha dalších formátů)
  • musí být nainstalovaný dodatečný software
  • může záviset na JNI
  • slabši podpora některých funkcí (makra apod.)
Excel JDBC driver
  • přístup k datům přes jednoduché rozhraní (JDBC)
  • čistá Java
  • velmi omezená funkcionalita
Java knihovny pro práci s XLS soubory
  • čistá Java
  • nezávisí na externím software
  • (často) zdrojové kódy k dispozici
tento přístup používám nejčastěji a nejraději
  • většinou jen základní funkcionalita
  • může nastat problém s komplikovanějšími soubory

Na jednotlivé přístupy se blíže podíváme v příštích dílech tohoto seriálu

čtvrtek 1. listopadu 2007

Zabezpečení webových aplikací

Nedávno jsem dostal za úkol implementovat autentizaci a autorizaci (přihlašování a implementace přístupových práv) v několika webových aplikacích u zákazníka. Aplikace sice využívají Hibernate, ale bohužel už ne Spring, pro který existuje security framework Acegi, poskytující přesně tu funkcionalitu, kterou potřebujeme. Acegi už jsem v jednom projektu úspěšně použil a tak jsem nebyl nadšenej z toho, že musím hledat jiné řešení.

Nakonec jsem část Acegi (zdrojáků) přeci jen použil. A to mírně upravené třídy z balíku org.acegisecurity.context, pomocí kterých lze jednoduše (s využitím servlet filtru) získat kdekoliv v aplikaci právě přihlášeného uživatele.

Další krok bylo vynucení přihlášení pro přístup k chráněným částem aplikace. Přemýšlel jsem o využití možností servlet kontejneru (např. Security Realm v Tomcatu), ale nevýhodou je, že pak pro deploy aplikace nestačí mít soubor s archivem (war), ale musíte také nakonfigurovat daný Realm přímo v instalaci Tomcatu. Navíc je toto řešení obtížně přenositelné mezi různými kontejnery, díky neexistující specifikaci, která by řešila detaily tohoto přístupu.

Nakonec jsem našel řešení v podobě knihovny SecurityFilter, která zajišťuje stejnou funkcionalitu jako Security Realm u Tomcatu, ale funguje na principu servlet filteru, takže se nemusíte zatěžovat konfigurováním servlet kontejneru ani se obávat nekompatibility mezi různými kontejnery. Stačí zaregistrovat filtr ve web.xml vytvořit implementaci pro interface org.securityfilter.realm.SecurityRealmInterface, a nakonfigurovat které role mají kam přístup a je to.

Jednoduchá implementace SecurityRealmInterface může vypadat následovně.

/**
* Returns object which returns Principal (in our case instance of class User).
* User which has given username and password is loaded by Hibernate and returned.
* @param username case insensitiv username
* @param password case sensitive password
* @see org.securityfilter.realm.SecurityRealmInterface#authenticate(java.lang.String, java.lang.String)
*/
public Principal authenticate(String username, String password) {
  log.debug("authenticate start");
  if (username==null || password==null) {
    log.info("username or password is null");
    return null;
  }
  final Session tmpSess = HibernateUtils.getCurrentSession();
  final boolean tmpInitTrans = ! tmpSess.getTransaction().isActive();
  if (tmpInitTrans) {
    tmpSess.beginTransaction();
  }
  final Query tmpQuery = tmpSess.createQuery(
      "FROM User u WHERE upper(u.username) = upper(:username)" +
      " AND u.password= :password");
  final User tmpUser =
    (User) tmpQuery
      .setString("username", username)
      .setString("password", password)
      .uniqueResult();
  if (tmpInitTrans) {
    tmpSess.getTransaction().commit();
  }
  return tmpUser;
}

/**
* Returns true if given user is in given role.
* @param user object of class User (see authenticate())
* @param role role name to test
* @return true if user is mapped to the role
* @see org.securityfilter.realm.SecurityRealmInterface#isUserInRole(Principal, String)
*/
public boolean isUserInRole(Principal user, String role) {
  if (user==null || role==null || !(user instanceof User)) {
    return false;
  }
  final User tmpUser = (User) user;
  final boolean tmpResult = tmpUser.isInRole(role);
  return tmpResult;
}

Třída User implementuje rozhraní java.security.Principal, což je snad ze zápisu zřejmé. Jestliže pracujete na tomcatu, je možné použít i realmy tomcatovské (např. org.apache.catalina.realm.JDBCRealm), pro které je v SecurityFiltru přibalen Catalina adaptér (org.securityfilter.realm.catalina.CatalinaRealmAdapter).

Kompletní konfigurační soubor securityfilter-config.xml v mém příkladu vypadá následovně:

<?xml version="1.0" encoding="ISO-8859-1"?>

<!DOCTYPE securityfilter-config PUBLIC
    "-//SecurityFilter.org//DTD Security Filter Configuration 2.0//EN"
    "http://www.securityfilter.org/dtd/securityfilter-config_2_0.dtd">

<securityfilter-config>

   <security-constraint>
      <web-resource-collection>
         <web-resource-name>Restricted pages</web-resource-name>
         <url-pattern>/protected/*</url-pattern>
      </web-resource-collection>
      <auth-constraint>
         <role-name>admin</role-name>
      </auth-constraint>
   </security-constraint>

   <login-config>
      <auth-method>FORM</auth-method>
      <form-login-config>
         <form-login-page>/login.do</form-login-page>
         <form-error-page>/loginError.jsp</form-error-page>
         <form-default-page>/start.do</form-default-page>
         <form-logout-page>/logout.jsp</form-logout-page>
      </form-login-config>
   </login-config>

   <realm className="cz.cacek.myapp.MyAppRealm"/>

</securityfilter-config>

A integrace do web.xml:

<filter>
 <filter-name>Security Filter</filter-name>
 <filter-class>org.securityfilter.filter.SecurityFilter</filter-class>
 <init-param>
  <description>Configuration file location (this is the default value)</description>
  <param-name>config</param-name>
  <param-value>/WEB-INF/securityfilter-config.xml</param-value>
 </init-param>
 <init-param>
  <description>Validate config file if set to true</description>
  <param-name>validate</param-name>
  <param-value>true</param-value>
 </init-param>
 <init-param>
  <description>
   As an example a login form can define "logMeIn" as it action in place of the standard
   "j_security_check" which is a special flag user by app servers for container managed security.
  </description>
  <param-name>loginSubmitPattern</param-name>
  <param-value>/logMeIn.do</param-value>
 </init-param>
</filter>

Ještě je potřeba tento filtr namapovat na požadovaný tvar URL nebo na servlet.

Při zabezpečování se nesmí zapomenout na kontrolu oprávnění na straně serveru. Opravdu není dostatečné řešení, kdy se pouze skryjí před neautorizovaným uživatelem URL na která by se neměl dostat. Co není povoleno, musí být zakázáno.

Stejně tak kontrola vstupních parametrů musí být samozřejmostí. Když je například editován objekt, ke kterému má daný uživatel právo, ale zlý uživatel v odesílaném formuláři změní ID objektu (bývá uloženo v hidden inputu) je dost dobře možné, že se mu podaří změnit cizí záznamy.

Metody zde popsané rozhodně nepřináší úplný výčet bodů pro zabezpečení webové aplikace. Pro další rady se můžete podívat do konference java.cz, kde se nedávno toto téma probíralo.

Linky: