14 Mayıs 2013 Salı

JSF Ajax Requestlerdeki Tarayıcı Geri Butonu Sorunu ve Dojo İle Çözümü

Yine JSF kodlama ve yeni bir sorun ile karşı karşıyayız :) Aslında problem tam olarak JSF ile ilgili değil. Bir web uygulaması içerisinde AJAX request gönderildiğinde, URL üzerinde herhangi bir değişiklik olmuyor ise, tarayıcılar, hash değeri değişmediği için, içinde bulunduğu sayfayı tarayıcının History stack'ine eklemiyor. Dolayısıyla siz aynı sayfanın üzerinde bir çok işlem yapsanız bile, tarayıcıdaki sayfa adresi değişmediği için geri ve ileri butonlarını kullanamıyoruz.

Bu probleme çözümler mevcut aslında. Çoğu kişi tarayıcı geri-ileri butonlarını kullanıcıya kullandırtmak yerine uygulama içerisinde kendisinin oluşturduğu geri-ileri butonlarını javascript metodları vasıtasıyla kullandırtıyor. Fakat yine de tarayıcının butonlarına bir çözüm bulunmalı ve mevcut sürece entegre edilmeli. Bu noktada, araştırıp, biraz modifiye ederek uygulamamda kullandığım bir yöntemden bahsedeceğim.

Primefaces 3.5 ve JSF 2.1 kullanarak geliştirdiğim bir uygulamada, Primefaces Datatable elemanı içerisinde yüklü bir listem var ve bir file system dizini gibi, her bir satıra tıklandığında, o satırdaki klasörün içerisine girilmesi gerekiyor. Geri ve ileri butonlarının da çalışması gerekiyor.

Bu noktada çözüm olarak dojo'nun back sınıfını kullandım. Bu kütüphane, her bir click eventi esnasında, history stack'ine bir veri kaydetmemizi sağlıyor. Bu veriyi de, geri ve ileri butonlarına tıklandığı zaman yapacağı işlemi tanımlayarak kaydediyoruz. Böylece kullanıcı geri butonuna bastığı zaman, kaydettiği parametrelerle işlem yapabiliyor.

Kodlarla özetlemek gerekirse:

myfiles.xhtml
<script>
     function HistoryState(fileid)
     {
           this.fileid = fileid;
           this.back = function() { 
                browserBck([{name: 'fileid', value: fileid}]);
           };
           this.forward = function() { 
               browserFrwrd([{name: 'fileid', value: fileid}]);
           };
           this.changeUrl = false;
       }
</script>

<script type="text/javascript"  src="js/dojo.js" djConfig="preventBackButtonFix: false"></script>
<script type="text/javascript">
     dojo.require("dojo.back");
     dojo.back.init();
     dojo.back.setInitialState(new HistoryState(null));
</script>

...<p:dataTable>... 
        <p:ajax event="rowSelect" listener="#{fileController.list()}"/> 
...</p:dataTable>

   <p:remoteCommand name="browserBck" action="{fileController.listBack()}"/> 

Yukarıdaki örnekte HistoryState adında bir nesne oluşturdum. Bu nesnede, dojo için gerekli olan back, forward ve changeUrl değişkenlerini set ettim. back değişkeni, geri butonuna tıklanınca yapılacakları, forward değişkeni ileri butonu için aynı işlemleri tanımlamamızı sağlıyor. changeUrl ise, her bir state kaydolurken dojo tarafından oluşturulan random rakamın adres satırına yazılıp yazılmamasını kontrol ediyor. HTML5 desteği olmayan tarayıcılar için bu değer true olmalı. Sayfa ilk defa render olurken ise setInitialState'i çalıştırıp işlemi başlatıyoruz. Yine aynı dosyada bulunan p:dataTable elemanının bir satırına tıklanınca server tarafında liste yenilemek için <p:ajax> kullandım:

FileController.java
public void list() {
        RequestContext rc = RequestContext.getCurrentInstance();
        rc.execute("dojo.back.addToHistory(new HistoryState('" + selectedFile.getId() + "'));");
        listByParentID(selectedFile.getId());
}

public void listBack() {
        ExternalContext ec = FacesContext.getCurrentInstance().getExternalContext(); 
        String fileID = ec.getRequestParameterMap().get("fileid");
        listByParentID(fileID);
} 

Yukarıdaki örnekte görüldüğü gibi, her bir listeleme işleminde, client tarafındaki addToHistory metoduna, HistoryState nesnemi ,içerisinde mevcut parent id bulunacak şekilde set ediyorum.

Bu örnek çok daha basitleştirilbilirdi fakat JSF ve server side'da detaylı bir örnek bulunsun istedim.

Süreci Özetlersek:
1 - xhtml dosyasında dojo'yu initialize ettik. History'ye kaydedeceğimiz nesneyi ve back-forwad metodlarımızı belirledik
2 - Bir linke tıklayıp server tarafına bir ajax request geçtik. Server tarafındaki metodda "RequestContext.getCurrentInstance().execute()" ile client tarafındaki dojo.back.addHistory metodunu çağırdık.
3 - Tarayıcının geri butonuna bastık. Bu işlem, 1. maddede belirlediğimiz back metodunu çağırdı (client-side).
4 - <p:remoteCommand> ile, 3. maddede çağrılan metodun, server tarafındaki başka bir metodu çağırmasını sağladık.
5 - Server tarafındaki metod ise istediğimiz listelemeyi yaptı ve geri butonumuz çalışmış oldu.

İleri butonu için de, forward nesnesine gerekli fonksiyonu tanımlayarak aynı işlemleri tekrarlamamız lazım.

Not: Bu şekilde çalışabilmesi için 'js' klasörünün içerisinde;
* dojo.js
* back.js
* resources/iframe_history.html

dosyalarının bulunması gerekiyor.

( jsf, primefaces, ajax, browser back button, forward, dojo, dojo.back, dojo.hash, request, xhtml, p:remoteCommand, p:dataTable )
Kaynak: http://blog.andreaskahler.com/2009/09/managing-browser-history-for-ajax-apps.html

7 Mayıs 2013 Salı

JSF ve HTML Escape Problemi, Çözümü

JSF teknolojisi, web uygulamaları geliştirirken standart olarak yazılımcının uygulaması gereken bir çok güvenlik açığını kendisi gidermektedir. Bunların en önemlilerinden birisi de XSS'i engellemek için render edilen HTML'de bulunan ve atak yapmaya müsait karakterleri escape etmesidir. Örneğin ">" şeklinde yazılan bir yazı, kaynak kodunda "&gt;" şeklinde gözükecektir. Bu özellik güvenlik açısından kolaylık sağlasa da bir takım kısıtlamalar da getirmektedir. Örneğin, yazılımcının kendisinin, xhtml içerisine yazacağı javascript ya da benzeri kod blokları, JSF'in bu özelliği sebebi ile ya compile olmaz, ya da ekranda sorunlu bir şekilde render edilir.

Bu problemin çözümü ise <h:outputText> kullanmaktır. Bu tag ile, JSF'in default davranışını bypass edebiliriz. Örneğin basit bir Javascript tagi kullanalım:
<script>alert('Test');</script>
Bu kod bloğunu direkt olarak xhtml içerisine yazarsak &lt;script&gt;alert('Test');&lt;/script&gt; gibi bir görüntü ile karşılaşırız ya da IDE'miz bu kodu derlemez, hata verir. Bu kodu önyüze taşımak için:
<h:outputText value="&lt;script&gt;alert('Test');&lt;/script&gt;" escape="false" />
şeklinde bir kullanımda bulunmalıyız. Burada, h:outputText içerisindeki değerin escape="false" ile escape edilmemesini sağlıyoruz. Burada value'yu bu şekilde vermek yerine normal bir şekilde yazarak bir String halinde tutup: <h:outputText value="#{fooController.myScript}" escape="false" /> şeklinde de kullanabilirsiniz.

Görüldüğü üzere yöntem oldukça basit. Fakat benim başıma gelen bir problem, bu şekilde bir çözüme gitmeyi engelledi ne yazık ki. Sorun şu şekilde; Internet Explorer versiyonlarına göre farklı CSS dosyaları kullanmak için html taginin başına:
"<!--[if lt IE 7]> <html lang=\"en-us\" class=\"no-js ie6\" xmlns:h="http://java.sun.com/jsf/html"> <![endif]-->..." 
tarzı kodlar yazarız. Bu kodları, yukarıda bahsettiğim şekilde escape etmek ne yazık ki mümkün değil çünkü kullandığımız <h:outputText>'in dahil olduğu "http://java.sun.com/jsf/html" namespace'ini, henüz tanımlamadan bu tagi kullanmak istiyoruz ki bu da mümkün değil. Burada da imdadımıza <f:view> tagi geliyor. <f:view> taginin içerisine namespace'lerimizi tanımladıktan sonra bütün içeriğimizi (<html> tagi vs. de dahil), <f:view> içerisine koyuyoruz. Böylece içeride <h:outputText>'i kullanabilir duruma geliyoruz ve sorunumuz çözülmüş oluyor. Daha anlaşılır olması için:

Sorun: Aşağıdaki kod bloğunu escape etmeden ön tarafa taşımak
<!--[if lt IE 7]>          <html lang="en-us" class="no-js ie6"> <![endif]-->
<!--[if IE 7]>             <html lang="en-us" class="no-js ie7"> <![endif]-->
<!--[if IE 8]>             <html lang="en-us" class="no-js ie8"> <![endif]-->
<!--[if gt IE 8]><!--> <html lang="en-us" class="no-js" xmlns="http://www.w3.org/1999/xhtml" xmlns:h="http://java.sun.com/jsf/html" xmlns:f="http://java.sun.com/jsf/core"> <!--<![endif]-->
Çözüm: Kod bloğunu <f:view> içerisine alıp namespace'leri buraya taşımak
@Controller("fooController")
FooController{
   public String myUnescapedString =
 
                    "<!--[if lt IE 7]><html lang=\"en-us\" class=\"no-js ie6\"> <![endif]-->\n" +
                    "<!--[if IE 7]><html lang=\"en-us\" class=\"no-js ie7\"> <![endif]-->\n" +
                    "<!--[if IE 8]><html lang=\"en-us\" class=\"no-js ie8\"> <![endif]-->\n" +
                    "<!--[if gt IE 8]><!--> <html lang=\"en-us\" class=\"no-js\"> <!--<![endif]-->";
}
<f:view contentType="text/html"
        xmlns="http://www.w3.org/1999/xhtml"
        xmlns:h="http://java.sun.com/jsf/html"
        xmlns:f="http://java.sun.com/jsf/core">
<h:outputText value="#{fooController.myUnescapedString}" escape="false"/>
</f:view>
( jsf, html, escape, h:outputText, f:view, unescape, xss )
Kaynak: http://stackoverflow.com/questions/10616944/jsf-2-1-ie-conditional-comments

7 Mart 2013 Perşembe

Ubuntu 12.10'a Oracle SQL Developer Kurulumu

Bu kurulum için ihtiyacımız olan bir kaç kurulum daha var. Öncelikle sistemde jre'nin kurulu olması gerekiyor. debhelper ve sqldeveloper-package paketlerine de ihtiyacımız var. Tabiki bir de ziplenmiş olarak indirdiğiniz SQL Developer'a ihtiyaç var.  Bu yazıda ben 'sqldeveloper64-3.2.20.09.87-no-jre.zip' kullanıyor olacağım.

sudo apt-get install sqldeveloper-package debhelper

Bu kodu çalıştırıp gerekli paketleri kurduktan sonra aşağıdaki komutla .deb dosyasını oluşturuyoruz:

make-sqldeveloper-package sqldeveloper64-3.2.20.09.87-no-jre.zip

Bu esnada make-sqldeveloper-package paketinde bulunan bir bugdan ötürü chmod: missing operand after `755' hatası alabilirsiniz. Bu hatayı çözmek için bu paketin kodunda ufak bir değişiklik yapmamız gerekiyor. Paketin versiyonuna göer değişiyor fakat 370 ile 385. satırlar arasında bir yerlerde bulunan aşağıdaki kodu, bir alttakiyle değiştirmeniz gerekiyor:

Silinecek Satır
${FIND} "${OPTDIR}" ! \( -type d -o -name "*.jar" \) |${XARGS} ${XARGS_OPTS} ${FILE} ${FILE_OPTS} |${GREP} ${GREP_OPTS} "shell script text executable" |${CUT} ${CUT_OPTS_FUNC_CLEAN} |${XARGS} ${XARGS_OPTS} ${CHMOD} ${CHMOD_OPTS}

Yerine Eklenecek Satır
${FIND} "${OPTDIR}" ! \( -type d -o -name "*.jar" \) |${XARGS} ${XARGS_OPTS} ${FILE} ${FILE_OPTS} |${GREP} ${GREP_OPTS} "shell script" | ${GREP} ${GREP_OPTS} "text executable" |${CUT} ${CUT_OPTS_FUNC_CLEAN} |${XARGS} ${XARGS_OPTS} ${CHMOD} ${CHMOD_OPTS}

Bu işlemden sonra hatadan kurtulmuş oluyoruz ve kodu yeniden çalıştırdığımızda .deb uzantılı dosyamız oluşmuş oluyor. Aşağıdaki şekilde .deb paketini açıyoruz:

sudo dpkg -i sqldeveloper.......deb

Böylece kurulum tamamlanmış oluyor. Dash Home'a "sqldeveloper" yazdıktan sonra uygulamamız açılacak. Yalnız yapmamız gereken son bir işlem daha var. Uygulama ilk kez açılırken size yüklü olan JRE'nin lokasyonunu soracak. Buraya da yüklediğimiz JRE'nin lokasyonunu yazacağız. Örneğin benimki şu şekildeydi:

/usr/lib/jvm/java-7-oracle

Artık IDE'miz kullanıma hazır durumdadır.

( ubuntu 12.04, oracle, sql developer, chmod, 755 )
Kaynak : https://bugs.launchpad.net/ubuntu/+source/sqldeveloper-package/+bug/985810

1 Mart 2013 Cuma

Spring MVC @Autowired NullPointerException

Spring MVC'de, nesnelerin yönetimini framework'e bırakma olayı sıkça kullanılan bir durumdur ve Spring'in ana kullanım amaçlarından birisidir. Bu kullanımlar esnasında, @Autowired annotation'unu kullanarak ya da getBean() metodu ile nesnelere ulaşmak istediğimizde alınan NullPointerException da oldukça sık karşılaşılan bir durumdur:) Ben yazının geri kalanında Annotation kullanımı ile devam edeceğim.

Burada, hataya sebebiyet verecek milyonlarca ihtimal vardır. Internette araştırdığınızda da bir dolu yazı karşınıza çıkacaktır. Bu sebeplerden bir kaçı:

  • @Autowired kulllandığınız nesneye sahip sınıfın Spring tarafından inject edilmemiş olması. Yani bu nesneye ait sınıfın başında @Component, @Controller gibi Spring'e özgü ve o sınıfın Spring tarafından yönetildiğini belirten annotationlar kullanılmalıdır.
  • Annotation kullanılan durumlarda, springContext.xml içerisinde '<tx:annotation-driven />' ve '<context:component-scan base-package />' taglerinin kullanılması gerekir.
  • Başına @Autowired eklediğiniz bir nesneyi 'new MyClass()' şeklinde kendiniz oluşturmamalısınız; bu nesnenin yönetimi artık Spring'dedir.

Yukarıdaki maddelere eklenecek başka maddeler de vardır. Bu hatalardan bir ya da birkaçına düştüyseniz kullanmak istediğiniz nesnenin null olması ihtimal dahilindedir. Peki herşeyi denediniz, bütün kodlama doğru ve hala bu hatayı alıyorsanız? O zaman benim karşılaştığım durumla karşılaşmış olma ihtimaliniz var. Spring MVC, kontrolüne verdiğiniz sınıfın instance'ını oluştururken, constructor'ı çalıştırma esnasında autowired nesneyi henüz inject etmiyor. Dolayısıyla bu nesneyi consturctor içerisinde kullanıyorsanız NullPointerException alıyorsunuz. Bu sorunun çözümü ise, eğer sınıfın instance'ını oluştururken yapmanız gereken bir iş yok ise, constructor içerisindeki bütün kod bloğunu @PostConstruct annotation'una sahip bir metoda vermeniz. Böylece ilgili nesne Spring tarafından oluşturulmuş ve kullanıma hazır hale gelmiş olacaktır. Aşağıda örnek kod parçalarını bulabilirsiniz.

Yanlış Kullanım

@Controller("userController")
public class UserController implements Serializable{
    @Autowired
    transient private SessionController sessionController;
    
    public UserController() {
        System.out.println(sessionController.getActiveUser().getUserName());
    }
}

Doğru Kullanım

@Controller("userController")
public class UserController implements Serializable{
    @Autowired
    transient private SessionController sessionController;
    
    public UserController() {}
    
    @PostConstruct
    public onLoad() {
        System.out.println(sessionController.getActiveUser().getUserName());
    }
}

spring mvc, injection, autowiring, @autowired, nullpointerexception, constructor, postconstruct )

20 Şubat 2013 Çarşamba

Spring Security Invalid Session Ajax Redirect Sorunu

Merhaba,

Bu aralar sıkça uğraştığım Spring Security hakkında yazdığım yazılara devam ediyorum. Bu seferki problem invalid hale gelmiş bir sessionda ajax request gönderdiğimizde view kısmında herhangi bir aksiyon olmaması. Önce problemin kaynağını belirteyim. Giden standart bir request'in response'ı ekrada düzgün bir şekilde gösterilirken giden bir ajax requestin response'ı partial-response olmalıdır. Uygulamada herhangi bir sayfadayken session timeout gerçekleştiğinde ve biz sonrasında ajax request gönderen bir işlem yaptığımızda spring security'nin standart jsf redirection metodu ajax response'ı redirect edemiyor. Bu noktada yapmamız gereken custom bir RedirectStrategy yazmak. Bu sınıf InvalidSessionStrategy sınıfını implement etmeli ve onInvalidSessionDetected metodunda ajax requesti yakalayıp ona göre farklı bir işlem yapmalıyız. İsterseniz burada http error gönderebilirsiniz. Ben bunun yerine bir partial response yazdım ve redirect işlemi gerçekleştirdim.

İhtiyacınız olacak örnek sınıf ve konfigürasyonları aşağıda bulabilirsiniz.

spring-security.xml
...
<http>
   <custom-filter ref="sessionManagementFilter" before="SESSION_MANAGEMENT_FILTER" />
</http>
<beans:bean id="sessionManagementFilter" class="org.springframework.security.web.session.SessionManagementFilter">
   <beans:constructor-arg name="securityContextRepository" ref="httpSessionSecurityContextRepository" />
   <beans:property name="invalidSessionStrategy" ref="customRedirectStrategy" />
</beans:bean>
<beans:bean id="customRedirectStrategy" class="com.test.JsfRedirectStrategy"/>
<beans:bean id="httpSessionSecurityContextRepository" class="org.springframework.security.web.context.HttpSessionSecurityContextRepository"/>
...

JsfRedirectStrategy.java
...
implements InvalidSessionStrategy
...
    @Override
    public void onInvalidSessionDetected(HttpServletRequest request, HttpServletResponse response){
        String redirectUrl = "invalid-session.xhtml";
        boolean ajaxRedirect = isAjaxRequest(request);
        if (ajaxRedirect) {
            if (request.getSession() != null) {
                HttpSessionRequestCache httpSessionRequestCache = new HttpSessionRequestCache();
                SavedRequest savedRequest = httpSessionRequestCache.getRequest(request, response);
                if (savedRequest != null) {
                    httpSessionRequestCache.removeRequest(request, response);
                }
            }
            String ajaxRedirectXml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><partial-response><redirect url=\"" + redirectUrl + "\"></redirect></partial-response>";
            response.setContentType("text/xml");
            PrintWriter out = response.getWriter();
           out.write(ajaxRedirectXml);
           out.flush();
           out.close();
        } else {
            response.sendRedirect(redirectUrl);
        }
    }

Yukarıda mavi renkli kod bloğunu sebebi ise AJAX targetURL'i silmek. Yoksa bir sonraki tıklamada ekrana kaydedilmiş AJAX requesti basılıyor. Eski Spring Security versiyonlarında direkt olarak URL'i de silebiliyorduk fakat 3.1.3 versiyonunda ben SavedRequest'i silerek bu işlemi gerçekleştirdim.

Bir diğer önemli konu ise, yukarıdaki işlemi ben yalnızca session invalidate olurkenki süreçte gerçekleştirdim. Bir de daha önce invalide olmuş bir session varken gönderilen bir AJAX request olması durumu olabilir (Örneğin yeni bir sekmede uygulamaya devam ediyorsunuz, logout yapıp sessionu öldürdünüz fakat bir önceki sekmede bulunan açık olan sayfada ajax request gönderen bir linke tıkladınız). Bu tarz bir durumda ise AuthenticationEntryPoint sınıfını implement edip commence metodunu aynı şekilde oluşturmalısınız. Bu yöntemle, hiç bir requesti kaçırmayıp, kullanıcıyı istediğiniz adrese yönlendirebilirsiniz.

Bir üstteki paragrafa örnek sınıf:

CustomAuthenticationEntryPoint.java
...
implements AuthenticationEntryPoint
...
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) {
        //Bir önceki sınıftaki kodun aynısı
    }

spring mvc, spring security, session management, invalid session, ajax redirect, jsf, redirect strategy, invalid session strategy )
Kaynak: http://code.google.com/a/apache-extras.org/p/ajax4click/wiki/ConfiguringSpringSecuritySessionTimeout
Kaynak: http://www.icesoft.org/wiki/display/ICE/Spring+Security