Monday, December 20, 2010

Subversion - merge changes in trunk and brunch from command line

Command line junkies can merge revisions from branch to trunk via svn command if they have a SVN command line client:
  1. Go to the project in the branch and show the log (via repo-browser or IDE).
  2. Look the revision at the beginning of your changes. Subtract 1 from the revision number.
  3. Look the revision at the end of your changes. Add 1 to the revision number.
  4. Go to the trunk working copy and type "svn merge -r <revision from step 2>:<revision from step 3> branchURL".
Example:
/svn/trunk/myproject> svn merge -r 30308:30648 svn+ssh://xyz.com:65000/var/svn/mycompany/dev/branches/1.0/myproject

Conflicts (if they exist) must be resolved manually.

Another tip to file comparision between two projects (brunch vs. trunk).

Goto the the trunk working copy and type
svn -c <change> svn+ssh://xyz.com:65000/var/svn/mycompany/dev/branches/1.0/myproject

This is a short command of
svn -r <change - 1>:<change> svn+ssh://xyz.com:65000/var/svn/mycompany/dev/branches/1.0/myproject
<change> is the revision number in the branch to be compared with the head revision in the trunk.

A bunch of changes can be compared with
svn -r <changeVon>:<changeBis> svn+ssh://xyz.com:65000/var/svn/mycompany/dev/branches/1.0/myproject
where <changeVon> is the start revision and <changeBis> is the end one.

The same operations can be done vice versa if you go to the brunch working copy and compare with trunk (by typing trunk URL).

Saturday, November 6, 2010

Install SUN Java-Plugin für Firefox under OpenSuse 11.x

IcedTea-Plugin is the standard Java-Pligin for Applets in OpenSuse. It's provided by openjdk. Some application, e.g. ElstarOnline, don't work with IcedTea and need Java-Plugin from SUN. Steps to install SUN's Java-Plugin:

1) Download SUN's JRE from http://www.java.com/de/download/. Current version is 1.6.0_21.

2) Install it. I've installed it under /usr/local/java/jre1.6.0_21

3) Create the following symbolic link in /usr/lib/browser-plugins/ (root rights are needed)
     sudo ln -s /usr/local/java/jre1.6.0_21/lib/i386/libnpjp2.so /usr/lib/browser-plugins/

4) Start Firefox and go to the Extras --> Add-ons --> Plugins

5) Deactivate IcedTea NPR Web Browser Plugin
    Activate (if not yet activated) Java(TM) Plug-in 1.6.0.21

Wednesday, October 27, 2010

Tests with PowerMock and MyFaces Test frameworks

In one of projects we use Mockito as test framework. It's a great framework having some limitations. We also use different mock objects for JSF environment which come from various sources. A not comfortable mishmash. I have decided to introduce PowerMock and MyFaces Test frameworks. This changing was very effective. First I want to compare two test approaches:
  • Old approach: Utilization of Mockito and JSF / Servlet mocks from "dead" Apache Shale, Spring, ebtam FacesContext.
  • Disadvantages: Limitations, no mocking of static, final, private classes / methods. Mix of different mocks causes an unconsistent environment. Hacks for the production code.
  • New approach: Utilization of PowerMock and MyFaces Test frameworks.
  • Advantages: Mocking of static, final, private classes / methods and more is possible. Unified mock objects and consistent weaving for all fundamental JSF / Servlet pendants.
PowerMock extends Mockito by using a custom classloader and bytecode manipulation. It enables mocking of static methods, constructors, final classes and methods, private methods and more. MyFaces Test Framework provides mock object libraries and base classes for creating own test cases. Mocks for the following APIs are provided:
  • JavaServer Faces
  • Servlet
The mock netting is as real. Created FacesContext instance will have been registered in the appropriate thread local variable, to simulate what a servlet container would do. The following section shows how to write power JSF 2 tests. For easing use of the new approach there is a class JsfServletMock which provides an access to JSF 2 and Servlet mocks.
package com.xyz.webapp.basemock;

import org.apache.myfaces.test.base.junit4.AbstractJsfTestCase;
import org.apache.myfaces.test.mock.*;
import org.apache.myfaces.test.mock.lifecycle.MockLifecycle;

public class JsfServletMock extends AbstractJsfTestCase
{
    public MockApplication20 mockApplication;
    public MockExternalContext20 mockExternalContext;
    public MockFacesContext20 mockFacesContext;
    public MockLifecycle mockLifecycle;
    public MockServletContext mockServletContext;
    public MockServletConfig mockServletConfig;
    public MockHttpSession mockHttpSession;
    public MockHttpServletRequest mockHttpServletRequest;
    public MockHttpServletResponse mockHttpServletResponse;

    public JsfServletMock() {
        try {
            super.setUp();
        } catch (Exception e) {
            throw new IllegalStateException("JSF / Servlet mocks could not be initialized");
        }

        // JSF API
        mockApplication = (MockApplication20) application;
        mockExternalContext = (MockExternalContext20) externalContext;
        mockFacesContext = (MockFacesContext20) facesContext;
        mockLifecycle = lifecycle;

        // Servlet API
        mockServletContext = servletContext;
        mockServletConfig = config;
        mockHttpSession = session;
        mockHttpServletRequest = request;
        mockHttpServletResponse = response;
    }
}
An object of this class can be used in tests per delegation pattern. Define the PowerMockRunner with @RunWith at the beginning. Define classes which static, final, private methods will be mocked / tested with @PrepareForTest. Take an example.
import com.xyz.webapp.basemock.JsfServletMock;
import com.xyz.webapp.util.FacesUtils;
import com.xyz.webapp.util.Observer;
import com.xyz.webapp.util.ObserverUtil;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;

import static org.junit.Assert.assertEquals;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.verify;
import static org.powermock.api.mockito.PowerMockito.*;

@RunWith(PowerMockRunner.class)
@PrepareForTest({FacesUtils.class, ObserverUtil.class})
public class DataExportWorkflowPageTest
{
    private JsfServletMock jsfServletMock;

    @Mock
    private StatusProfileFinder statusProfileFinder;

    @Mock
    private Collection<Observer> observers;

    private DataExportWorkflowPage dewp = new DataExportWorkflowPage();

    @Before
    public void before() {
        jsfServletMock = new JsfServletMock();

        dewp.setObservers(observers);
        dewp.setStatusProfileFinder(statusProfileFinder);
    }

    @Test
    public void initialize() {
        mockStatic(FacesUtils.class);

        DataExportHistoryForm dehf = mock(DataExportHistoryForm.class);
        DataExportContentForm decf = mock(DataExportContentForm.class);

        WorkflowData workflowData = mock(WorkflowData.class);

        when(FacesUtils.accessManagedBean("dataExportHistoryForm")).thenReturn(dehf);
        when(FacesUtils.accessManagedBean("dataExportContentForm")).thenReturn(decf);

        List<StatusProfile> spList = new ArrayList<StatusProfile>();
        StatusProfile sp1 = new StatusProfile(GenericObjectType.DOCUMENT);
        sp1.setBusinessKey("document");
        spList.add(sp1);
        StatusProfile sp2 = new StatusProfile(GenericObjectType.ACTIVITY);
        sp2.setBusinessKey("activity");
        spList.add(sp2);
        StatusProfile sp3 = new StatusProfile(GenericObjectType.EXPENSE_DOCUMENT_ITEM);
        sp3.setBusinessKey("expense");
        spList.add(sp3);

        jsfServletMock.mockHttpSession.setAttribute(Constants.SESSION_CLIENT, new Client());

        when(statusProfileFinder.getAllLimitBy(any(Client.class), eq(Constants.MAX_RESULTS_DEFAULT))).thenReturn(spList);
        when(FacesUtils.accessManagedBean("workflowData")).thenReturn(workflowData);

        dewp.initialize();

        verify(observers).add(dehf);
        verify(observers).add(decf);
        verifyNoMoreInteractions(observers);

        assertEquals(GenericObjectType.DOCUMENT.getId(), dewp.getStatusProfile());
        assertEquals(workflowData, dewp.getWorkflowData());
    }

    ...
}
After JsfServletMock instantiation in a method annotated with @Before we have a full access to mock objects. E.g. MockHttpSession can be accessed by jsfServletMock.mockHttpSession. In the example I want to mock the static method FacesUtils.accessManagedBean(String) in the static class. We have to write first
mockStatic(FacesUtils.class);
to achieve that. And then quite normally
when(FacesUtils.accessManagedBean(...)).thenReturn(...);
In another small example I use the call
verifyStatic();
to verify if the subsequent static call got really called. In the example below we verify if
ObserverUtil.notifyAll(depd);
was called within
depd.setProcedureStep2(null);
@RunWith(PowerMockRunner.class)
@PrepareForTest(ObserverUtil.class)
public class DataExportProcedureDataTest
{
    private DataExportProcedureData depd;

    @Mock
    private Collection<Observer> observers;

    @Before
    public void before() {
        depd = new DataExportProcedureData();
        depd.setObservers(observers);
    }

    @Test
    public void setProcedureStep2() {
        mockStatic(ObserverUtil.class);

        depd.setProcedureStep2(null);

        verifyStatic();
        ObserverUtil.notifyAll(depd);
    }

    ...
}
You see we can do both - mock static methods and verify behaviors.

Bring power to JSF tests! No need for workarounds any more! Keep your production code clean from the test driven stuff!

Tuesday, October 19, 2010

Global handling of all unchecked / unexpected exceptions in JSF 2

JSF 2 specification introduced a new ExceptionHandler API. All exceptions can be handled globally by an ExceptionHandler instance. Advantage of this centralization is that it allows developers to devise more error handling strategies. I was inspired by the great article of Ed Burns "Dealing Gracefully with ViewExpiredException in JSF2" and developed my own central Exception Handler.
Firstly, we need a factory class that creates and returns a new Exception Handler instance.
public class DefaultExceptionHandlerFactory extends ExceptionHandlerFactory {
    private ExceptionHandlerFactory parent;

    public DefaultExceptionHandlerFactory(ExceptionHandlerFactory parent) {
        this.parent = parent;
    }

    @Override
    public ExceptionHandler getExceptionHandler() {
        ExceptionHandler eh = parent.getExceptionHandler();
        eh = new DefaultExceptionHandler(eh);

        return eh;
    }
}
This factory class has to be registered in faces-config.xml
<factory>
    <exception-handler-factory>
        ip.client.jsftoolkit.commons.DefaultExceptionHandlerFactory
    </exception-handler-factory>
</factory>
Secondly, we need a class DefaultExceptionHandler. This is the default implementation of the exception handler to catch unchecked / unexpected exceptions in order to proper display.
public class DefaultExceptionHandler extends ExceptionHandlerWrapper {
    private static final Log LOG = LogFactory.getLog(DefaultExceptionHandler.class);

    /** key for session scoped message detail */
    public static final String MESSAGE_DETAIL_KEY = "ip.client.jsftoolkit.messageDetail";

    private ExceptionHandler wrapped;

    public DefaultExceptionHandler(ExceptionHandler wrapped) {
        this.wrapped = wrapped;
    }

    @Override
    public ExceptionHandler getWrapped() {  
        return this.wrapped;
    }

    @Override
    public void handle() throws FacesException {
        if (fc.isProjectStage(ProjectStage.Development)) {
            // Code for development mode. E.g. let the parent handle exceptions
            getWrapped().handle();
        } else {
            for (Iterator<ExceptionQueuedEvent> i = getUnhandledExceptionQueuedEvents().iterator(); i.hasNext();) {
                ExceptionQueuedEvent event = i.next();
                ExceptionQueuedEventContext context = (ExceptionQueuedEventContext) event.getSource();

                String redirectPage = null;
                FacesContext fc = FacesContext.getCurrentInstance();
                Throwable t = context.getException();

                try {
                    if (t instanceof AbortProcessingException) {
                        // about AbortProcessingException see JSF 2 spec.
                        LOG.error("An unexpected exception has occurred by event listener(s)", t);
                        redirectPage = "/views/error.jsf?statusCode=jsftoolkit.exception.UncheckedException";
                        fc.getExternalContext().getSessionMap()
                            .put(DefaultExceptionHandler.MESSAGE_DETAIL_KEY, t.getLocalizedMessage());
                    } else if (t instanceof ViewExpiredException) {
                        if (LOG.isDebugEnabled()) {
                            LOG.debug("View '" + ((ViewExpiredException) t).getViewId() + "' is expired", t);
                        }

                        ApplicationConfiguration appConfiguration =
                            (ApplicationConfiguration) FacesAccessor.accessManagedBean(
                                ApplicationConfiguration.BEAN_NAME_APPLICATION_CONFIGURATION);
                        HttpSession session = (HttpSession) fc.getExternalContext().getSession(false);
                        if (session != null) {
                            // should not happen
                            session.invalidate();
                        }

                        if (appConfiguration.getBoolean(ConfigKeys.KEY_LOGOFF_2_LOGOUT_PAGE, false)) {
                            // redirect to the specified logout page
                            redirectPage = "/views/logout.jsf";
                        } else {
                            // redirect to the login page
                            redirectPage = "";
                        }
                    } else if (t instanceof ServiceNotAvailableException) {
                        LOG.error("'" + ((ServiceNotAvailableException) t).getServiceName() + "' is not available", t);
                            redirectPage = "/views/error.jsf?statusCode=jsftoolkit.exception.ServiceNotAvailableException";
                    } else {
                        // custom handling of unexpected exceptions can be done in the method handleUnexpected
                        String messageKey = handleUnexpected(fc, t);
                        redirectPage = "/views/error.jsf?statusCode=" + messageKey;
                        fc.getExternalContext().getSessionMap()
                            .put(DefaultExceptionHandler.MESSAGE_DETAIL_KEY, t.getLocalizedMessage());
                    }
                } finally {
                    i.remove();
                }

                SecurityPhaseListener spl = new SecurityPhaseListener();
                spl.doRedirect(fc, redirectPage);

                break;
        }
    }

    protected String handleUnexpected(FacesContext facesContext, final Throwable t) {
        LOG.error("An unexpected internal error has occurred", t);

        return "jsftoolkit.exception.UncheckedException";
    }
}
The method handleUnexpected(FacesContext, Throwable) can be overwritten in derived classes in order to customization. Handling of various unexpected exceptions can be done there. An example of using:
protected String handleUnexpected(FacesContext facesContext, final Throwable t) {
    if (t instanceof IllegalStateException) {
        // some special handling here
        ...
        return "key.exception.IllegalStateException";
    } else {
        return super.handleUnexpected(facesContext, t);
    }
}
SecurityPhaseListener is the phase listener from my last post allowing JSF redirects.

JSF Ajax redirect after session timeout

JSF 2 has a facility to be able to do Ajax redirects. The specification says - an element <redirect url="redirect url"/> found in the response causes a redirect to the URL "redirect url". JSF component libraries often write other "redirect" elements into response, but the end result is the same - a redirect happens. It works great in case of JSF Ajax redirects.
A web application normally has a security filter - an own solution or from Spring framework or something else. Such security filter does a redirect if user is not authenticated to use requested web resources. This is not an JSF redirect. The filter doesn't pass current request to the FacesServlet at all. Here is an example:
public class SecurityFilter implements Filter {

    private Authenticator authenticator;
    private String loginPage;
    
    public void init(FilterConfig config) throws ServletException {
        ...
        // create an Authenticator  
        authenticator = AuthenticatorFactory.createAuthenticator(config);
        loginPage = getLoginPage(config);
        ...
    }

    public void doFilter(ServletRequest request, ServletResponse response,
        FilterChain chain) throws IOException, ServletException {
        HttpServletRequest hReq = (HttpServletRequest) request;
        HttpServletResponse hRes = (HttpServletResponse) response;
        ...
        // get user roles
        Collection roles = ... 
        // get principal
        Principal principal = hReq.getUserPrincipal();
        
        if (!roles.isEmpty() && principal == null) {
            // user needs to be authenticated
            boolean bRet = authenticator.showLogin(hReq, hRes);
            if (!bRet) {
         // redirect the a login page or error sending
                ...
            }
        }
        ...
    }
}
Problem: indeed an Ajax request is redirected to a specified page after session timeout, but the response can not be proper treated on the client-side. We have a quite regular redirect in this case and the specified page in the response. Client-side Ajax code doesn't expect the entire page coming back.
A possible solution would be to pass "not authenticated" request through FacesServlet. We need to write an Authenticator class which stores the page URL we want to be redirected to in the request scope.
public class FormAuthenticator {
    ...

    public boolean showLogin(HttpServletRequest request, 
        HttpServletResponse response, String loginPage) throws IOException {
        ...
        request.setAttribute("web.secfilter.authenticator.showLogin", loginPage);
 
        return true;
    }
}
The method showLogin returns true in order to avoid a redirect by filter. Let us execute the redirect by JSF! The call gets now a third parameter "loginPage".
authenticator.showLogin(hReq, hRes, loginPage);
If you use Spring security you should have an entry point in your XML configuration file like this one
<beans:bean id="authenticationProcessingFilterEntryPoint"
       class="com.xyz.webapp.security.AuthenticationProcessingFilterEntryPoint">
    <beans:property name="loginFormUrl" value="/login.html"/>
    <beans:property name="serverSideRedirect" value="true"/>
</beans:bean>
The class AuthenticationProcessingFilterEntryPoint is derived from Spring's one
/**
 * Extended Spring AuthenticationProcessingFilterEntryPoint for playing together with JSF Ajax redirects.
 */
public class AuthenticationProcessingFilterEntryPoint extends
    org.springframework.security.ui.webapp.AuthenticationProcessingFilterEntryPoint
{
    public static final String ATTRIBUTE_LOGIN_PAGE = "web.secfilter.authenticator.showLogin";

    public void commence(ServletRequest request, ServletResponse response, AuthenticationException authException)
        throws IOException, ServletException {

        request.setAttribute(ATTRIBUTE_LOGIN_PAGE, getLoginFormUrl());
        super.commence(request, response, authException);
    }
}
On the JSF side we need a special phase listener to do the redirect by ExternalContext. We check at first the buffered page URL in the beforePhase. If it was set - an JSF redirect is needed and will be done.
/**
 * Phase listener for the Restore View Phase which manages display of login page.
 */
public class SecurityPhaseListener implements PhaseListener
{
    private static final Log LOG = LogFactory.getLog(SecurityPhaseListener.class);

    public void afterPhase(PhaseEvent event) { ; }

    public void beforePhase(PhaseEvent event) {
        FacesContext fc = event.getFacesContext();

        String loginPage = (String) fc.getExternalContext().getRequestMap().
            get("web.secfilter.authenticator.showLogin");
        if (StringUtils.isNotBlank(loginPage)) {
            doRedirect(fc, loginPage);
        }
    }

    public PhaseId getPhaseId() {
        return PhaseId.RESTORE_VIEW;
    }

    /**
     * Does a regular or ajax redirect.
     */
    public void doRedirect(FacesContext fc, String redirectPage) 
        throws FacesException {
        ExternalContext ec = fc.getExternalContext();

        try {
            if (ec.isResponseCommitted()) {
                // redirect is not possible
                return;
            }

            // fix for renderer kit (Mojarra's and PrimeFaces's ajax redirect)
            if ((RequestContext.getCurrentInstance().isAjaxRequest()
                || fc.getPartialViewContext().isPartialRequest())
                && fc.getResponseWriter() == null
                && fc.getRenderKit() == null) {
                    ServletResponse response = (ServletResponse) ec.getResponse();
                    ServletRequest request = (ServletRequest) ec.getRequest();
                    response.setCharacterEncoding(request.getCharacterEncoding());

                    RenderKitFactory factory = (RenderKitFactory)  
                     FactoryFinder.getFactory(FactoryFinder.RENDER_KIT_FACTORY);

                    RenderKit renderKit = factory.getRenderKit(fc,
                     fc.getApplication().getViewHandler().calculateRenderKitId(fc));

                    ResponseWriter responseWriter =
                        renderKit.createResponseWriter(
                        response.getWriter(), null, request.getCharacterEncoding());
                        fc.setResponseWriter(responseWriter);
            }

            ec.redirect(ec.getRequestContextPath() + 
                (redirectPage != null ? redirectPage : ""));
        } catch (IOException e) {
            LOG.error("Redirect to the specified page '" + 
                redirectPage + "' failed");
            throw new FacesException(e);
        }
    }
}
It works in both cases - for Ajax and regular request.

Sunday, October 3, 2010

Install Dropbox under OpenSuse 11.3

Dropbox is a great online tool to share data between computers. The software for OpenSuse can be found either here http://software.opensuse.org/113/en and installed directly or you have to add this repository
http://download.opensuse.org/repositories/openSUSE:/11.3:/Contrib/standard/
to the YAST and install it by YAST. After installation open a terminal and type
dropbox start -i
Just follow the prompts to create an account and choose a dropbox folder on your PC.

Tuesday, September 21, 2010

Install Skype under OpenSuse 11.x

If you have installed Skype and it crashes while starting you maybe have an old version of Skype. Here are steps to install Skype on Linux:

1) Delete currently installed Skype
zypper rm skype

2) Delete hidden file .skype in the user home directory.

3) Make sure you have installed libpng12-0 package. Install it by YaST if needed.

4) Grab the current Skype version from http://www.skype.com/intl/en/get-skype/on-your-computer/linux/ and install it. You can also install it directly, e.g.
rpm -Uvh http://download.skype.com/linux/skype-2.1.0.81-suse.i586.rpm

WLAN with Dell notebook and OpenSuse 11.x

Dell notebooks are normally equipped with Broadcom card for WLAN. All 802.11n Broadcom devices need to use the Broadcom STA driver. You need to accomplish the following steps to install such drivers:

1) Download current drivers from http://packman.links2linux.de/package/broadcom-wl. You need two drivers. In my case
broadcom-wl-5.60.48.36-7.pm.8.5.i586.rpm
broadcom-wl-kmp-default-5.60.48.36_k2.6.34.7_0.2-7.pm.8.5.i586.rpm

2) Open Linux terminal and go to the directory where you downloaded these drivers. Let run two commandos one after another
rpm -ivh broadcom-wl-kmp-default-5.60.48.36_k2.6.34.7_0.2-7.pm.8.5.i586.rpm
rpm -ivh broadcom-wl-5.60.48.36-7.pm.8.5.i586.rpm --force

3) Reboot your PC. If you still don't have a WLAN connection go to the YaST and configure WLAN in network devices. You should see at least your drivers.

Assists in creating consistent equals(), hashCode(), toString()

I often see many different implementations for methods equals(), hashCode() and toString(). There is a good and simple approach in Apache Commons Lang which I always use for this purpose. Classes EqualsBuilder, HashCodeBuilder and ToStringBuilder from the package org.apache.commons.lang.builder provide methods to build consistent and good equals() hashCode() and toString() methods of any class.
We can use either Java reflection or call successive append method for our purpose. Assume we have the following Java class:
public class MyClass {
    private String name;
    private int count;
    private List keys;
    ...
}
1) Use Java reflection:
@Override
public boolean equals(final Object obj)
{
    return EqualsBuilder.reflectionEquals(this, obj);
}

@Override
public int hashCode()
{
    return HashCodeBuilder.reflectionHashCode(this);
}

@Override
public String toString()
{
    return ToStringBuilder.reflectionToString(this);
}
2) Include fields manually:
@Override
public boolean equals(final Object obj)
{
    if (obj == null) {
        return false;
    }

    if (obj == this) {
        return true;
    }

    if (obj.getClass() != getClass()) {
        return false;
    }

    MyClass mc = (MyClass) mc;

    return new EqualsBuilder().append(name, mc.name).append(count, mc.count).append(keys, mc.keys).isEquals();
}

@Override
public int hashCode()
{
    return new HashCodeBuilder(17, 37).append(name).append(count).append(keys).toHashCode();
}

@Override
public String toString()
{
    return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE).
                               append("name", name).append("count", count).append("keys", keys).toString();
}
In the second case we have more control and can decide which fields we want to compare and which not. And don't care about data types. They can be primitive, Collection, Map or something else.

Thursday, August 5, 2010

Replacement of h:head in order to fix resources rendering

I came up against need to reimplement JSF 2 head component by reason of an issue. I have implemented my custom head tag and achieved more flexibility. I'm going to begin with a history before I will show the code. I'm working with PrimeFaces component library. PrimeFaces comes with predefined CSS settings. Developers often want to overwrite these predefined styles. I do that quite simple - include PrimeFaces resources inside of the head tag and include my own resources close to the body tag. Including of PrimeFaces resources (by p:resource) is actually not necessary for regular requests with full page refresh. But it's necessary if you have partial updates via ajax. An output looks like
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"><head>
<link rel="stylesheet" type="text/css" href="/icManagement/primefaces_resource/2.0.2/yui/tabview/assets/skins/sam/tabview.css" />
<link rel="stylesheet" type="text/css" href="/icManagement/primefaces_resource/2.0.2/yui/accordion/assets/skins/sam/accordionview.css" />
<link rel="stylesheet" type="text/css" href="/icManagement/primefaces_resource/2.0.2/yui/menu/assets/skins/sam/menu.css" />
<script type="text/javascript" src="/icManagement/primefaces_resource/2.0.2/yui/menu/menu-min.js"></script>
<script type="text/javascript" src="/icManagement/primefaces_resource/2.0.2/yui/json/json-min.js"></script>
...
<link type="text/css" rel="stylesheet" href="/icManagement/javax.faces.resource/sky/css/ip-jsftoolkit.css.jsf?ln=themes" />
<link type="text/css" rel="stylesheet" href="/icManagement/javax.faces.resource/sky/css/incursa-management.css.jsf?ln=themes" />
<script type="text/javascript" src="/icManagement/javax.faces.resource/ip-jsftoolkit.js.jsf?ln=js"></script>
<script type="text/javascript" src="/icManagement/javax.faces.resource/incursa-management.js.jsf?ln=js"></script>
Custom CSS, JavaScript files are places after PrimeFaces' one. Well. Mojarra 2.0.2 had an issue, that CSS selectors placed in h:head cannot override styles in implicitly added resources. This issue was fixed in Mojarra 2.0.3. But this bugfixing is not enough because it fixed one issue and caused another one. Why? Often we need <meta> tags to prevent page caching or make sites search engine friendly. Using the <link> tag you can add a "canonical" url to your page and a favicon. The order of these additional tags can be very important. Internet Explorer introduced e.g. a special <meta> tag
<meta http-equiv="X-UA-Compatible" content="..." />
Content of the X-UA-Compatible <meta> tag helps to control document compatibility. We can specify the rendering engine. For example, inserting this:
<meta http-equiv="X-UA-Compatible" content="IE=8" />
into the head of a document would force IE8 renders the page using the new standards mode. I often heard "IE8 breaks my site". Add this <meta> tag or HTTP header which can fix the site:
<meta http-equiv="X-UA-Compatible" content="IE=7" />
It fixes your site, at least until you can sort out the IE8 issues. I use on my pages some great features which are supported by IE8 only. For instance, IE6 / IE7 don't support border color for select elements. You cannot set border color for h:selectOneMenu or h:selectManyLisbox. Unfortunately, IE8 was rendering my pages in IE7 mode. DOCTYPE was ok, but maybe Apache settings were fault or something else. I decided to put content="IE=8" on all my XHTML pages. But it didn't have any effect. The reason: X-UA-Compatible must be the first child of the head! Internet Explorer doesn't accept this <meta> tag if it's placed after <link> or <script> tags. Many people also encountered the same problem and trey tried making the <meta> tag X-UA-Compatible as the first child of the head to solve this problem. But how could I place this <meta> tag quite at the beginning? The standard h:head tag of the last Mojarra implementation doesn't have this possibility. It always renders at first resources added by JSF 2 facility (don't matter what is placed within head). My output was similar this:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"><head>
<link rel="stylesheet" type="text/css" href="/icManagement/primefaces_resource/2.0.2/yui/tabview/assets/skins/sam/tabview.css" />
<link rel="stylesheet" type="text/css" href="/icManagement/primefaces_resource/2.0.2/yui/accordion/assets/skins/sam/accordionview.css" />
<link rel="stylesheet" type="text/css" href="/icManagement/primefaces_resource/2.0.2/yui/menu/assets/skins/sam/menu.css" />
<script type="text/javascript" src="/icManagement/primefaces_resource/2.0.2/yui/menu/menu-min.js"></script>
<script type="text/javascript" src="/icManagement/primefaces_resource/2.0.2/yui/json/json-min.js"></script>
...
<meta http-equiv="X-UA-Compatible" content="IE=8"/>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<meta http-equiv="pragma" content="no-cache"/>
<meta http-equiv="cache-control" content="no-cache"/>
<meta http-equiv="expires" content="0"/>
You see, the standrad h:head is not flexible enough. Oracle ADF has a special <af:document> tag where you can use the facet "metaContainer" of the <af:document> to output the tags in the right place. Eureka! We need facets in the head component for right order controling of rendered resources. Let's write a custom head component:
public class Head extends UIOutput
{
 public static final String COMPONENT_TYPE = "ip.client.jsftoolkit.components.Head";
 public static final String COMPONENT_FAMILY = "ip.client.jsftoolkit.components";
 private static final String DEFAULT_RENDERER = "ip.client.jsftoolkit.components.HeadRenderer";

 /**
  * Properties that are tracked by state saving.
  */
 protected enum PropertyKeys
 {
  title, shortcutIcon;

  String toString;

  PropertyKeys(String toString)
  {
   this.toString = toString;
  }

  PropertyKeys()
  {
  }

  public String toString()
  {
   return ((this.toString != null) ? this.toString : super.toString());
  }
 }

 public Head()
 {
  setRendererType(DEFAULT_RENDERER);
 }

 public String getFamily()
 {
  return COMPONENT_FAMILY;
 }

 public String getTitle()
 {
  return (String) getStateHelper().eval(PropertyKeys.title, null);
 }

 public void setTitle(String title)
 {
  getStateHelper().put(PropertyKeys.title, title);
 }

 public String getShortcutIcon()
 {
  return (String) getStateHelper().eval(PropertyKeys.shortcutIcon, null);
 }

 public void setShortcutIcon(String shortcutIcon)
 {
  getStateHelper().put(PropertyKeys.shortcutIcon, shortcutIcon);
 }
}
I took "title" and "shortcutIcon" attributes to the head component in order to achieve more convenience by using. Now the renderer:
import java.io.IOException;
import java.util.ListIterator;
import javax.faces.component.UIComponent;
import javax.faces.component.UIViewRoot;
import javax.faces.context.FacesContext;
import javax.faces.context.ResponseWriter;
import javax.faces.render.Renderer;

public class HeadRenderer extends Renderer
{
 public void encodeBegin(FacesContext facesContext, UIComponent component) throws IOException
 {
  ResponseWriter writer = facesContext.getResponseWriter();
  writer.startElement("head", component);

  UIComponent first = component.getFacet("first");
  if (first != null) {
   first.encodeAll(facesContext);
  }

  UIViewRoot viewRoot = facesContext.getViewRoot();
  ListIterator<UIComponent> iter = (viewRoot.getComponentResources(facesContext, "head")).listIterator();
  while (iter.hasNext()) {
   UIComponent resource = (UIComponent) iter.next();
   resource.encodeAll(facesContext);
  }
 }

 public void encodeChildren(FacesContext facesContext, UIComponent component) throws IOException
 {
 }

 public void encodeEnd(FacesContext facesContext, UIComponent component) throws IOException
 {
  ResponseWriter writer = facesContext.getResponseWriter();
  Head head = (Head) component;

  UIComponent last = component.getFacet("last");
  if (last != null) {
   last.encodeAll(facesContext);
  }

  if (head.getTitle() != null) {
   writer.startElement("title", null);
   writer.write(head.getTitle());
   writer.endElement("title");
  }

  if (head.getShortcutIcon() != null) {
   writer.startElement("link", null);
   writer.writeAttribute("rel", "shortcut icon", null);
   writer.writeAttribute("href", head.getShortcutIcon(), null);
   writer.endElement("link");
  }

  writer.endElement("head");
 }
}
I introduced two facets called "first" and "last". The resource rendering order with this approach:
  1. Resources placed into "first" facet.
  2. Resources added by JSF 2 facility.
  3. Resources placed into "last" facet.
  4. Page title and shortcut icon.
The new component and its tag have be registered as usual in faces-config and taglib XMLs.
<renderer>
 <component-family>ip.client.jsftoolkit.components</component-family>
 <renderer-type>ip.client.jsftoolkit.components.HeadRenderer</renderer-type>
 <renderer-class>ip.client.jsftoolkit.components.head.HeadRenderer</renderer-class>
</renderer>
<component>
 <component-type>ip.client.jsftoolkit.components.Head</component-type>
 <component-class>ip.client.jsftoolkit.components.head.Head</component-class>
</component>

<tag>
 <tag-name>head</tag-name>
 <component>
  <component-type>ip.client.jsftoolkit.components.Head</component-type>
  <renderer-type>ip.client.jsftoolkit.components.HeadRenderer</renderer-type>
 </component>
</tag>
Using on page:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
   xmlns:f="http://java.sun.com/jsf/core"
   xmlns:h="http://java.sun.com/jsf/html"
   xmlns:ui="http://java.sun.com/jsf/facelets"
   xmlns:jtcomp="http://ip.client/ip-jsftoolkit/components">
<f:view contentType="text/html" locale="#{userSettings.locale}">
<ui:insert name="metadata"/>
<jtcomp:head title="#{messageParameters['frameworkTitle']}" shortcutIcon="#{request.contextPath}/favicon.ico">
  <f:facet name="first">
    <meta http-equiv="X-UA-Compatible" content="IE=8"/>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
    <meta http-equiv="pragma" content="no-cache"/>
    <meta http-equiv="cache-control" content="no-cache"/>
    <meta http-equiv="expires" content="0"/>
  </f:facet>
  <f:facet name="last">
    <style type="text/css">
      html, body {
        width: 100%;
        height: 100%;
        margin: 0px;
      }
    </style>
    <script type="text/javascript">...</script>
  </f:facet>
  <ui:insert name="resources-pf"/>
</jtcomp:head>
<h:body>
...
 <ui:insert name="resources-app"/>
</h:body>
</f:view>
</html>
Generated output in the right defined order:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"><head>
  <meta http-equiv="X-UA-Compatible" content="IE=8" />
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
  <meta http-equiv="pragma" content="no-cache" />
  <meta http-equiv="cache-control" content="no-cache" />
  <meta http-equiv="expires" content="0" />
  
  <link rel="stylesheet" type="text/css" href="/icManagement/primefaces_resource/2.0.2/yui/tabview/assets/skins/sam/tabview.css" />
  <link rel="stylesheet" type="text/css" href="/icManagement/primefaces_resource/2.0.2/yui/accordion/assets/skins/sam/accordionview.css" />
  <link rel="stylesheet" type="text/css" href="/icManagement/primefaces_resource/2.0.2/yui/menu/assets/skins/sam/menu.css" />
  <script type="text/javascript" src="/icManagement/primefaces_resource/2.0.2/yui/menu/menu-min.js"></script>
  <script type="text/javascript" src="/icManagement/primefaces_resource/2.0.2/yui/json/json-min.js"></script>
  ...
  <link type="text/css" rel="stylesheet" href="/icManagement/javax.faces.resource/sky/css/ip-jsftoolkit.css.jsf?ln=themes" />
  <link type="text/css" rel="stylesheet" href="/icManagement/javax.faces.resource/sky/css/incursa-management.css.jsf?ln=themes" />
  <script type="text/javascript" src="/icManagement/javax.faces.resource/ip-jsftoolkit.js.jsf?ln=js"></script>
  <script type="text/javascript" src="/icManagement/javax.faces.resource/incursa-management.js.jsf?ln=js"></script>
  <style type="text/css">
   html, body {
    width: 100%;
    height: 100%;
    margin: 0px;
   }
  </style>
  <script type="text/javascript">...</script>
  <title>Management Client</title>
  <link rel="shortcut icon" href="/icManagement/favicon.ico" />
  </head>
...
Cool? :-)

Wednesday, August 4, 2010

Metadata extraction from MS Outlook messages

In the previous post I showed how to extract metadata from MS Office documents. In this post I will show how to extract metadata from MS Outlook messages. There are some solutions like Apache POI-HSMF oder msgparser. But all these solutions are not universal enough, API are restricted and algorithm are not reliable because they try to find / match some subsequences without deep knowledge about MSG file structure. My task was to extract the properties "Subject", "From", "To", "Cc", "Sent", "Received". Well. Let's start with MsgExtractor. The method to be called after instantiation is parseMetaData. The idea is similar to the one for MS Office documents.
public class MsgExtractor
{
 public static final String PROPERTY_SUBJECT = "Subject";
 public static final String PROPERTY_FROM = "From";
 public static final String PROPERTY_TO = "To";
 public static final String PROPERTY_CC = "Cc";
 public static final String PROPERTY_SENT = "Sent";
 public static final String PROPERTY_RECEIVED = "Received";

 private static final Map TEMPLATE_PROPERTY_2_GET_METHODS;

 static {
  TEMPLATE_PROPERTY_2_GET_METHODS = new HashMap();
  TEMPLATE_PROPERTY_2_GET_METHODS.put(PROPERTY_SUBJECT, "getSubject");
  TEMPLATE_PROPERTY_2_GET_METHODS.put(PROPERTY_FROM, "getDisplayFrom");
  TEMPLATE_PROPERTY_2_GET_METHODS.put(PROPERTY_TO, "getDisplayTo");
  TEMPLATE_PROPERTY_2_GET_METHODS.put(PROPERTY_CC, "getDisplayCc");
  TEMPLATE_PROPERTY_2_GET_METHODS.put(PROPERTY_SENT, "getClientSubmitTime");
  TEMPLATE_PROPERTY_2_GET_METHODS.put(PROPERTY_RECEIVED, "getMessageDeliveryTime");
 }
 
 final String[] properties;

 public MsgExtractor(final String[] properties)
 {
  this.properties = (properties == null ? new String[] {} : properties);
 }

 public Map parseMetaData(final byte[] data) throws MetaDataExtractionException
 {
  // check at first whether byte array data is null and throw an exception if it's null
  ...
  
  if (properties.length == 0) {
   return Collections.EMPTY_MAP;
  }

  Map metaData = new HashMap();
  OutlookMessage outlookMessage = new OutlookMessage(new ByteArrayInputStream(data));

  try {
   for (int i = 0; i < properties.length; i++) {
    Object propertyValue = null;
    String strMethod = (String) TEMPLATE_PROPERTY_2_GET_METHODS.get(properties[i]);
    if (strMethod != null) {
     Method method = OutlookMessage.class.getMethod(strMethod, (Class[]) null);
     propertyValue = method.invoke(outlookMessage, (Object[]) (Object[]) null);
    } else {
     LOG.warn("No Get-Method to the MAPI-Property was found for the defined template property '"
              + properties[i] + "'");
    }

    // buffer property values
    metaData.put(properties[i], propertyValue);
   }
  } catch (final Exception e) {
   // failed to extract meta data ==> error handling
   ...
  }

  return metaData;
 }
}
The main class is OutlookMessage which hides a magic work. It looks as follows
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.util.Date;
import java.util.Iterator;

import org.apache.commons.io.EndianUtils;
import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.poi.poifs.filesystem.DirectoryEntry;
import org.apache.poi.poifs.filesystem.DocumentEntry;
import org.apache.poi.poifs.filesystem.DocumentInputStream;
import org.apache.poi.poifs.filesystem.Entry;
import org.apache.poi.poifs.filesystem.POIFSFileSystem;

**
 * Reads an Outlook MSG File in and provides hooks into its data structure. Some hints to the structure were found under
 *
 * <ul>
 *   <li>http://www.fileformat.info/format/outlookmsg/</li>
 *   <li>http://svn.apache.org/viewvc/poi/trunk/src/scratchpad/src/org/apache/poi/hsmf/</li>
 *   <li>http://www.tech-archive.net/Archive/Development/microsoft.public.win32.programmer.ole/2006-08/msg00123.html
 *   </li>
 * </ul>
 *
 */
public class OutlookMessage
{
 private static final Log LOG = LogFactory.getLog(OutlookMessage.class);

 /** id for the mapi property "subject" */
 private static final int PROP_SUBJECT = 0x0037;

 /** id for the mapi property "display from" */
 private static final int PROP_DISPLAY_FROM = 0x0C1A;

 /** id for the mapi property "display to" */
 private static final int PROP_DISPLAY_TO = 0x0E04;

 /** id for the mapi property "display cc" */
 private static final int PROP_DISPLAY_CC = 0x0E03;

 /** reversed endian format of the property id "client submit time" */
 private static final byte[] PROP_CLIENT_SUBMIT_TIME = {0x40, 0x00, 0x39, 0x00};

 /** reversed endian format of the property id "message delivery time" */
 private static final byte[] PROP_MESSAGE_DELIVERY_TIME = {0x40, 0x00, 0x06, 0x0E};

 private String subject;

 private String displayFrom;

 private String displayTo;

 private String displayCc;

 private Date clientSubmitTime;

 private Date messageDeliveryTime;

 public OutlookMessage(String filename) throws IOException
 {
  this(new FileInputStream(new File(filename)));
 }

 public OutlookMessage(InputStream in)
 {
  try {
   POIFSFileSystem fs = new POIFSFileSystem(in);
   initMapiProperties(fs);
  } catch (IOException ioe) {
   LOG.warn("Some properties could be not parsed from given MSG message " + ioe);
  } finally {
   if (in != null) {
    try {
     in.close();
    } catch (IOException e) {
     ;
    }
   }
  }
 }

 private void initMapiProperties(POIFSFileSystem fs) throws IOException
 {
  DirectoryEntry root = fs.getRoot();

  for (Iterator iter = root.getEntries(); iter.hasNext();) {
   Entry entry = (Entry) iter.next();
   if (!(entry instanceof DocumentEntry)) {
    continue;
   }

   String entryName = entry.getName();
   if (entryName == null) {
    continue;
   }

   // parse MAPI properties
   if (entryName.startsWith("__substg1.0_")) {
    byte[] substgBytes = getBytes((DocumentEntry) entry);

    int id = Integer.parseInt(entryName.substring(12, 16), 16);
    int type = Integer.parseInt(entryName.substring(16, 20), 16);

    if (id == PROP_SUBJECT) {
     // subject
     this.subject = getString(substgBytes, isUnicodeString(type));
    } else if (id == PROP_DISPLAY_FROM) {
     // display from
     this.displayFrom = getString(substgBytes, isUnicodeString(type));
    } else if (id == PROP_DISPLAY_TO) {
     // display to
     this.displayTo = getString(substgBytes, isUnicodeString(type));
    } else if (id == PROP_DISPLAY_CC) {
     // display cc
     this.displayCc = getString(substgBytes, isUnicodeString(type));
    }
   } else if (entryName.startsWith("__properties_version1.0")) {
    byte[] propBytes = getBytes((DocumentEntry) entry);
    int offset = 0;
    int bytesLength = propBytes.length;

    while (offset + 16 <= bytesLength) {
     byte[] propId = read4(propBytes, offset);

     if (compare(propId, PROP_CLIENT_SUBMIT_TIME)) {
      // read value
      this.clientSubmitTime = getDate(propBytes, offset);
     } else if (compare(propId, PROP_MESSAGE_DELIVERY_TIME)) {
      // read value
      this.messageDeliveryTime = getDate(propBytes, offset);
     }

     offset = offset + 16;
    }
   }
  }
 }

 private byte[] getBytes(DocumentEntry docEntry) throws IOException
 {
  DocumentInputStream dis = new DocumentInputStream(docEntry);
  byte[] propBytes = new byte[dis.available()];

  try {
   byte[] bytes = new byte[4096];
   int readCount;
   int curPosition = 0;
   while ((readCount = dis.read(bytes)) > -1) {
    System.arraycopy(bytes, 0, propBytes, curPosition, readCount);
    curPosition = curPosition + readCount;
   }
  } finally {
   dis.close();
  }

  return propBytes;
 }

 private String getString(byte[] bytes, boolean isUnicode)
 {
  if (ArrayUtils.isEmpty(bytes)) {
   return null;
  }

  try {
   String str;
   if (isUnicode) {
    str = new String(bytes, 0, bytes.length, "UTF-16LE");
   } else {
    str = new String(bytes, 0, bytes.length, "ISO8859_1");
   }

   int len = str.length();
   while (len > 0 && str.charAt(len - 1) == '\0') {
    len--;
   }

   if (len != str.length()) {
    str = str.substring(0, len);
   }

   if (StringUtils.isBlank(str)) {
    str = null;
   }

   return str;
  } catch (UnsupportedEncodingException ignore) {
   ;
  }

  return null;
 }

 private boolean isUnicodeString(int type)
 {
  return (type == 0x001F ? true : false); // for not unicode string type = 0x001E
 }

 private byte[] read4(byte[] data, int offset)
 {
  byte[] readBytes = new byte[4];
  System.arraycopy(data, offset, readBytes, 0, 4);

  return readBytes;
 }

 private byte[] read8(byte[] data, int offset)
 {
  byte[] readBytes = new byte[8];
  System.arraycopy(data, offset, readBytes, 0, 8);

  return readBytes;
 }

 private boolean compare(byte[] b1, byte[] b2)
 {
  for (int i = 0; i < b1.length; ++i) {
   if (b1[i] != b2[i]) {
    return false;
   }
  }

  return true;
 }

 private Date getDate(byte[] propBytes, int offset)
 {
  // read value
  byte[] value = read8(propBytes, offset + 8);

  // convert to long (reverse Endian format)
  long time = EndianUtils.readSwappedLong(value, 0);

  // FILETIME 64-bit int number of 100ns periods since Jan 1, 1601 ==>
  // convert ns to ms and substruct milliseconds between 1/1/1601 and 1/1/1970
  time = (time / 10 / 1000) - 1000L * 60L * 60L * 24L * (365L * 369L + 89L);

  return new Date(time);
 }
 
 // getter

 public String getSubject() {return subject;}

 public String getDisplayFrom() {return displayFrom;}

 public String getDisplayTo() {return displayTo;}

 public String getDisplayCc() {return displayCc;}

 public Date getClientSubmitTime() {return clientSubmitTime;}

 public Date getMessageDeliveryTime() {return messageDeliveryTime;}
}
Parse of MAPI properties in the stream __substg1.0_ don't cause any problems. That are quite normally strings. But some properties are in a binary stream called __properties_version1.0. This is the most interesting stream.
Hint to the structure of __properties_version1.0: Each MAPI property has a well documented hex id, available on the MSDN site. For example, the property PR_CLIENT_SUBMIT_TIME has an identifier of 0x00390040. If you open the properties stream and work with 16 byte rows, divided into two 8 byte sections (just like a hex editor), you will see that the property identifier is in the first 8 byte section and the value is in the second. The trick is to reverse the Endian of the property ID. So the ID for the PR_CLIENT_SUBMIT_TIME property becomes 40 00 39 00. Locate this hex block and you will have the value (in Big Endian FILETIME format) in the next 8 byte section.

With a little code extension you can extract "creation time" (reversed endian format of the property id {0x40, 0x00, 0x07, 0x30}), "last modification time" (reversed endian format of the property id {0x40, 0x00, 0x08, 0x30}) or all other imaginable properties. You can use a Viewer for MS Outlook Messages to find out proper endian formats. I have tried this approach for MSG files produced with MS Outlook 2003 / 2007, for unicode / non unicode messages and any other combinations. It was working in all cases.

Tuesday, July 27, 2010

Metadata extraction from MS Office documents with Apache POI

Microsoft Office documents have several metadata or properties like "Title", "Author", "Comments", "Keywords", "CreateDateTime", "LastSaveDateTime", etc. Apache POI HPSF, Java API for Microsoft documents, is a neat library to extract such properties from Word, Excel or PowerPoint documents. It can be useful if user upload MS Office files and would like to show / edit metadata before uploaded files are stored in a content repository or somewhere else. Let's write an Java class named MsOfficeExtractor.
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import org.apache.poi.hpsf.PropertySetFactory;
import org.apache.poi.hpsf.SummaryInformation;
import org.apache.poi.poifs.eventfilesystem.POIFSReader;
import org.apache.poi.poifs.eventfilesystem.POIFSReaderEvent;
import org.apache.poi.poifs.eventfilesystem.POIFSReaderListener;

public class MsOfficeExtractor
{
 private String[] properties;
 private Map<String, Method> methodMap;

 public MsOfficeExtractor(final String[] properties)
 {
  this.properties = (properties == null ? new String[] {} : properties);
  methodMap = new HashMap<String, Method>();
  try {
   for (int i = 0; i < properties.length; i++) {
    methodMap.put(properties[i], SummaryInformation.class.getMethod("get" + properties[i], (Class[]) null));
   }
  } catch (SecurityException e) {
   // error handling
  } catch (NoSuchMethodException e) {
   // error handling
  }
 }
 
 public Map<String, Object> parseMetaData(final byte[] data)
 {
  if (properties.length == 0) {
   return Collections.EMPTY_MAP;
  }

  InputStream in = null;
  try {
   in = new ByteArrayInputStream(data);
   POIFSReader poifsReader = new POIFSReader();
   MetaDataListener metaDataListener = new MetaDataListener();
   poifsReader.registerListener(metaDataListener, "\005SummaryInformation");
   poifsReader.read(in);

   return metaDataListener.metaData;
  } catch (final IOException e) {
   // error handling
  } catch (final RuntimeException e) {
   // error handling
  } finally {
   if (in != null) {
    try {
     in.close();
    } catch (IOException e) {
     // nothing to do
    }
   }
  }
 }
}
The constructor expects property names of the properties we want to extract. All valid names are defined in org.apache.poi.hpsf.SummaryInformation. The map methodMap defines a mapping between a property to be extracted and a method to be called in the listener explained below. The core method is parseMetaData which expects a byte array of given MS Office file. We need now a listener class MetaDataListener which is called while parsing.
public class MetaDataListener implements POIFSReaderListener
{
 public final Map<String, Object> metaData;

 public MetaDataListener()
 {
  metaData = new HashMap<String, Object>();
 }

 public void processPOIFSReaderEvent(final POIFSReaderEvent event)
 {
  try {
   final SummaryInformation summaryInformation = (SummaryInformation) PropertySetFactory.create(event.getStream());

   for (int i = 0; i < properties.length; i++) {
    Method method = (Method) methodMap.get(properties[i]);
    Object propertyValue = method.invoke(summaryInformation, (Object[]) (Object[]) null);

    metaData.put(properties[i], propertyValue);
   }
  } catch (final Exception e) {
   // error handling
  }
 }
}
The goal of this listener is to build a map with extracted values to the given properties (which values we want to extract). The using is simple:
// initialize extractor
String[] poiProperties = new String[] {"Comments", "CreateDateTime", "LastSaveDateTime"};
MsOfficeExtractor msOfficeExtractor = new MsOfficeExtractor(poiProperties);

// get byte array of any MS office document
byte[] data = ...

// extract metadata
Map<String, Object> metadata = msOfficeExtractor.parseMetaData(data);
In the next post I will show how to extract metadata from MS Outlook msg files. Apache POI-HSMF, Java API to access MS Outlook msg files, has limits and is not flexible enough. I will present my own powerful solution.

Wednesday, July 21, 2010

Encode / decode parameters for GET request

As is well-known GET request parameters (query string) must be encoded on client-side and decoded on server-side. Normally an application server is responsible for decoding URL parameters. But in the real world I already had a case where parameters were not decoded (maybe some settings were wrong). Well. The client-side solution which I wrote for my web apps is to pass request parameters through this function
// Replaces all special characters in the passed string.
function encodeString(thestring)
{
    var encodedValue;
    if(window.encodeURIComponent) {
        encodedValue = encodeURIComponent(thestring);
    } else {
        encodedValue = escape(thestring);
        encodedValue = encodedValue.replace(new RegExp('\\+', 'g'), '%2B');
    }
    return encodedValue;
}
The Java based server-side solution (if needed) is easy.
String encoding = request.getCharacterEncoding();
if (encoding == null) {
    encoding = "UTF-8";
}

try {
    String decodedParameter = java.net.URLDecoder.decode(originalParameter, encoding);
    ...
} catch (UnsupportedEncodingException e) {
    // do something
}

What element mouse comes from or goes to

Sometimes we want to know where the mouse comes from or goes to in case of mouseover / mouseout event. Assume, we have assigned to any HTML element a mouseout listener mouseOut. We can easy acquire the source (from element) and target (to element) by fired event.
function mouseOut(evt)
{
    evt = (evt) ? evt : ((window.event) ? window.event : "");
    var fromElement = (evt.target) ? evt.target : evt.srcElement;
    var toElement = (evt.relatedTarget) ? evt.relatedTarget : evt.toElement;
    
    // do something with fromElement / toElement
}
Easy. Is not?

A cross-browser solution to get iframe body element

An often task while web development is the working with iframe. If we have iframe's body object bodyIframe we can use quite normally JavaScript means to do for instance its content empty
...
bodyIframe.innerHTML = "";
...
or to get any HTML form like
...
var form = bodyIframe.getElementsByTagName("form")[0];
...
or something else. Unfortunately there isn't an uniform access to iframe's body object. It's dependent on browser. I have found out an universal way to get this element by iframe Id (<iframe id="..." .../>). The appropriate JS function looks as follows
function getIframeBody(iframeId)
{
   var obj = document.getElementById(iframeId);
   if (obj == null) {
      return null;
   }

   var bodyIframe = null;
   if (obj.contentWindow && obj.contentWindow.document.body) {
      bodyIframe = obj.contentWindow.document.body;
   } else if (obj.document && obj.document.body) {
      bodyIframe = obj.document.body;
   } else if (obj.contentDocument && obj.contentDocument.body) {
      bodyIframe = obj.contentDocument.body;
   }

   return bodyIframe;
}

Tuesday, July 20, 2010

How to access Jackrabbit content repository via JNDI?

In this post I described how an Jackrabbit content repository can be set up. I will show now how it can be accessed via JNDI. I dont' want to write an JNDI lookup although it's not difficult. I would like to use Google Guice, a dependency injection framework. At first we need a Guice configuration module.
public class DefaultConfigurationGuiceModule extends AbstractModule
{
    protected void configure()
    {
        bind(String.class).annotatedWith(Names.named("repository name")).toInstance("jcr/repository");

        // bind naming context to the default InitialContext
        bind(Context.class).to(InitialContext.class);

        // bind to the repository from JNDI
        bind(Repository.class).toProvider(JndiIntegration.fromJndi(Repository.class, "jcr/repository"));

        // bind to the factory class for the creation of repository accessor
        // see http://code.google.com/docreader/#p=google-guice&s=google-guice&t=AssistedInject
        bind(RepositoryAccessorFactory.class).toProvider(FactoryProvider.newFactory(RepositoryAccessorFactory.class,
                                             JackrabbitRepositoryAccessor.class)).in(Singleton.class);

    }
}
Guice has a helpful class JndiIntegration to create a provider which looks up objects in JNDI using the given name. Furthermore I use Guice's AssistedInject and define a factory interface to create an instance for repository access (JackrabbitRepositoryAccessor). The real factory will be created by AssistedInject.
public interface RepositoryAccessorFactory
{
    /**
     * Greates an instance of {@link RepositoryAccessor}. Sets an input stream of XML file which describes custom node
     * types and appropriated custom namespace mapping. Custom node types and namespace will be registered one-time if
     * the JCR session is requested and they were not registered yet.
     *
     * @param  nodeTypeConfigs configurations of custom node types to be registered
     * @return RepositoryAccessor the instance of {@link RepositoryAccessor}
     */
    RepositoryAccessor create(@Assisted final NodeTypeConfig[] nodeTypeConfigs);
}
The class NodeTypeConfig is used for the registration of custom node types. Node types are described here. More about custom node types in XML notation see in my previous post.
/**
 * Configuration infos about a node type to be registered.
 */
public class NodeTypeConfig
{
    /** input stream of XML file which describes node types */
    private InputStream inputStream;

    /** namespace prefix of the node type */
    private String namespacePrefix;

    /** namespace uri of the node type */
    private String namespaceUri;

    /**
     * Creates a new NodeTypeConfig object.
     *
     * @param inputStream     input stream of XML file which describes node types
     * @param namespacePrefix namespace prefix of the node type
     * @param namespaceUri    namespace uri of the node type
     */
    public NodeTypeConfig(final InputStream inputStream, final String namespacePrefix, final String namespaceUri)
    {
        this.inputStream = inputStream;
        this.namespacePrefix = namespacePrefix;
        this.namespaceUri = namespaceUri;
    }

    setter / getter ...

    /**
    * Loads node type configuration from XML file in classpath.
    *
    * @param  fileName        file name
    * @param  namespacePrefix namespace prefix of the node type
    * @param  namespaceUri    namespace uri of the node type
    * @return NodeTypeConfig configuration
    */
    public static NodeTypeConfig getNodeTypeConfig(final String fileName, final String namespacePrefix, final String namespaceUri)
    {
        InputStream inputStream = getInputStreamConfig(fileName);
        return new NodeTypeConfig(inputStream, namespacePrefix, namespaceUri);
    }

    /**
     * Gets input stream from XML file in classpath.
     *
     * @param  fileName file name
     * @return NodeTypeConfig configuration
     */
    public static InputStream getInputStreamConfig(final String fileName)
    {
        Validate.notNull(fileName, "XML file with node type configuration is null");

        ClassLoader classloader = Thread.currentThread().getContextClassLoader();
        if (classloader == null) {
            classloader = NodeTypeConfig.class.getClassLoader();
        }

        return classloader.getResourceAsStream(fileName);
    }
}
The most important class is JackrabbitRepositoryAccessor. This is an entry point into the content repository. This class implements an interface RepositoryAccessor. This interface looks as follows
public interface RepositoryAccessor
{
 /**
  * Gets the content repository. If no repository has been yet created it will be created.
  *
  * @see    #startRepository()
  * @return Repository repository {@link Repository}
  * @throws RepositoryException if the repository could be not acquired
  */
 Repository getRepository() throws RepositoryException;

 /**
  * Starts and initializes the content repository by the configured repository name via JNDI.
  *
  * @throws RepositoryException if the repository could be not acquired or an error occured
  */
 void startRepository() throws RepositoryException;

 /**
  * Retrieves the current JCR Session local to the thread which it is tied to one workspase. If no JCR Session is
  * open, opens a new JCR Session for the running thread.
  *
  * @param  workspaceName name of the workspace (<code>null</code> is not allowed)
  * @param  viewerId      viewer id from {@link SecurityToken}
  * @return Session JCR session {@link Session}
  * @throws LoginException            if the login fails
  * @throws NoSuchWorkspaceException  if a specific workspace is not found
  * @throws AccessDeniedException     if the session associated with the workspace object does not have sufficient
  *                                   permissions to register core / custom namespaces, to create a new workspace or
  *                                   some access-related methods failed
  * @throws NamespaceException        if an illegal attempt is made to register a mapping
  * @throws RegisterNodeTypeException if registration of core / custom node type(s) failed
  * @throws RepositoryException       if the repository could be not acquired or an error occured
  */
 Session getSession(final String workspaceName, final String viewerId)
     throws LoginException, NoSuchWorkspaceException, AccessDeniedException, NamespaceException,
            RegisterNodeTypeException, RepositoryException;

 /**
  * Closes all JCR Sessions local to the thread.
  */
 void releaseSession();

 /**
  * Releases the content repository
  */
 void releaseRepository();
}
And the implementation (a little bit big code) looks as follows
public class JackrabbitRepositoryAccessor implements RepositoryAccessor
{
 private static final Logger LOG = LoggerFactory.getLogger(RepositoryAccessor.class);

 private static final ThreadLocal<Map<String, Session>> THREAD_SESSION = new ThreadLocal<Map<String, Session>>();

 /** repository instance */
 private Repository repository;

 /** defauilt workspace */
 private Workspace defaultWorkspace;

 /** repository name (not mandatory) */
 @Inject(optional = true)
 @Named("repository name")
 private String repositoryName;

 /** flag whether the core namespace mapping and node types were already registered */
 private boolean isRegistered = false;

 /** flag whether the custom namespace mapping and node types were already registered */
 private boolean isCustomRegistered = false;

 /** input stream of XML file which describes custom node types (not mandatory) */
 private NodeTypeConfig[] customNodeTypeConfigs;

 /** provider for repository */
 private Provider<Repository> repositoryProvider;

 /**
  * Creates a new <code>JackrabbitRepositoryAccessor</code> object and sets repository providers.
  * Note: Custom node types and namespace will be registered one-time if the JCR session is
  * requested and they were not registered yet.
  *
  * @param repositoryProvider                  repository provider to get an access to the configured repository
  *                                            {@link Repository}
  * @param customNodeTypeConfigs               custom node types configurations (if <code>null</code> no custom node
  *                                            types will be registered)
  */
 @Inject
 public JackrabbitRepositoryAccessor(final Provider<Repository> repositoryProvider,
                                     @Assisted
                                     @Nullable
                                     final NodeTypeConfig[] customNodeTypeConfigs)
 {
  // set repository provider
  this.repositoryProvider = repositoryProvider;

  this.customNodeTypeConfigs = customNodeTypeConfigs;
 }

 //~ Methods ----------------------------------------------------------------

 /**
  * Gets the default workspace. If no default workspace has been yet created it will be created.
  *
  * @see    #startRepository()
  * @return Workspace default workspace {@link Workspace}
  * @throws RepositoryException if the repository or workspace could be not acquired
  */
 protected Workspace getDefaultWorkspace() throws RepositoryException
 {
  if (defaultWorkspace == null) {
   synchronized (JackrabbitRepositoryAccessor.class) {
    Repository repository = getRepository();
    if (defaultWorkspace == null) {
     defaultWorkspace = repository.login().getWorkspace();
     if (LOG.isDebugEnabled()) {
      LOG.debug("==> Default workspace '"
                + (defaultWorkspace != null ? defaultWorkspace.getName() : "null")
                + "' acquired.");
     }
    }
   }
  }

  return defaultWorkspace;
 }

 /**
  * Registers a node type.
  *
  * @param  jcrSession  current JCR session
  * @param  inputStream input stream of XML file which describes node types
  * @throws RegisterNodeTypeException if registration of core / custom node type failed
  * @throws RepositoryException       if an error occured
  */
 @SuppressWarnings("unchecked")
 protected void registerNodeType(final Session jcrSession, final InputStream inputStream)
     throws RegisterNodeTypeException, RepositoryException
 {
  try {
   NodeTypeManagerImpl ntManager = (NodeTypeManagerImpl) jcrSession.getWorkspace().getNodeTypeManager();
   NodeTypeRegistry ntRegistry = ntManager.getNodeTypeRegistry();
   NodeTypeDefStore ntDefStore = new NodeTypeDefStore();

   ntDefStore.load(inputStream);

   Collection<NodeTypeDef> ntDefs = ntDefStore.all();
   Iterator<NodeTypeDef> iter = ntDefs.iterator();
   while (iter.hasNext()) {
    NodeTypeDef ntDef = iter.next();
    if (!ntRegistry.isRegistered(ntDef.getName())) {
     ntRegistry.registerNodeType(ntDef);
    }
   }
  } catch (IOException e) {
   throw new RegisterNodeTypeException(e);
  } catch (InvalidNodeTypeDefException e) {
   throw new RegisterNodeTypeException(e);
  } finally {
   IOUtils.closeQuietly(inputStream);
  }
 }
 
 /**
  * {@inheritDoc}
  */
 public Repository getRepository() throws RepositoryException
 {
  if (repository == null) {
   synchronized (JackrabbitRepositoryAccessor.class) {
    if (repository == null) {
     startRepository();
    }
   }
  }

  return repository;
 }

 /**
  * {@inheritDoc}
  */
 public void startRepository() throws RepositoryException
 {
  try {
   repository = repositoryProvider.get();

   if (repository == null) {
    throw new RepositoryException("Unable to acquire Repository '" + repositoryName
                                  + "' via JNDI");
   }

   if (LOG.isDebugEnabled()) {
    LOG.debug("==> Repository started.");
   }

   // get default workspace (it's always available)
   defaultWorkspace = repository.login().getWorkspace();
   if (LOG.isDebugEnabled()) {
    LOG.debug("==> Default workspace '" + (defaultWorkspace != null ? defaultWorkspace.getName() : "null")
              + "' acquired.");
   }
  } catch (Throwable t) {
   throw new RepositoryException("Unable to acquire Repository '" + repositoryName
                                 + "' via JNDI", t);
  }
 }

 /**
  * {@inheritDoc}
  */
 public Session getSession(final String workspaceName, final String viewerId)
     throws LoginException, NoSuchWorkspaceException, AccessDeniedException, NamespaceException,
            RegisterNodeTypeException, RepositoryException
 {
  if (workspaceName == null) {
   throw new NoSuchWorkspaceException("Workspace name is null. JCR Session can be not opened.");
  }

  Session jcrSession = null;
  Map<String, Session> workspace2Session = THREAD_SESSION.get();
  if (workspace2Session == null) {
   workspace2Session = new HashMap<String, Session>();
  } else {
   jcrSession = workspace2Session.get(workspaceName);
  }

  if (jcrSession != null && !jcrSession.isLive()) {
   jcrSession = null;
  }

  if (jcrSession == null) {
   if (LOG.isDebugEnabled()) {
    LOG.debug("==> Opening new JCR Session for the current thread.");
   }

   SimpleCredentials credentials = new SimpleCredentials(viewerId, "".toCharArray());
   try {
    // authentication to get jcr session
    jcrSession = getRepository().login(credentials, workspaceName);
   } catch (NoSuchWorkspaceException e) {
    // try to create new workspace with the given name because it doesn't exist yet
    Workspace workspace = getDefaultWorkspace();
    if (workspace == null) {
     throw new NoSuchWorkspaceException("Default workspace could be not created. JCR Session can be not opened.");
    }

    if (LOG.isDebugEnabled()) {
     LOG.debug("==> Try to create workspace '" + workspaceName + "'.");
    }

    // create new workspace
    ((JackrabbitWorkspace) workspace).createWorkspace(workspaceName);
    if (LOG.isDebugEnabled()) {
     LOG.debug("==> Workspace '" + workspaceName + "' has been created.");
    }

    // authentication again to get jcr session
    jcrSession = getRepository().login(credentials, workspaceName);
   }

   if (jcrSession == null) {
    throw new LoginException("JCR Session could be not opened (null).");
   }

   workspace2Session.put(workspaceName, jcrSession);
   THREAD_SESSION.set(workspace2Session);
  }

  // register core namespace mapping and node types if they were not registered yet
  if (!isRegistered) {
   synchronized (JackrabbitRepositoryAccessor.class) {
    if (!isRegistered) {
     NamespaceRegistry namespaceRegistry = jcrSession.getWorkspace().getNamespaceRegistry();

     // check whether the namespace prefix or uri already exist
     if (!ArrayUtils.contains(namespaceRegistry.getPrefixes(), Constants.NAMESPACE_PREFIX)
         || !ArrayUtils.contains(namespaceRegistry.getURIs(), Constants.NAMESPACE_URI)) {
      // register namespace
      namespaceRegistry.registerNamespace(Constants.NAMESPACE_PREFIX, Constants.NAMESPACE_URI);
      if (LOG.isDebugEnabled()) {
       LOG.debug("Namespace prefix '" + Constants.NAMESPACE_PREFIX
                 + "' has been registered to the uri '"
                 + Constants.NAMESPACE_URI + "'");
      }
     }

     // register core node types!
     InputStream inputStream = NodeTypeConfig.getInputStreamConfig("core_node_types.xml");
     if (inputStream == null) {
      LOG.error("Node type definition 'core_node_types.xml' was not found");
      throw new RegisterNodeTypeException("Node type definition 'core_node_types.xml' was not found");
     }

     registerNodeType(jcrSession, inputStream);

     if (LOG.isDebugEnabled()) {
      LOG.debug("Register of core node types is ensured");
     }

     isRegistered = true;
    }
   }
  }

  // register core namespace mapping and node types if they were not registered yet
  if (!isCustomRegistered) {
   synchronized (JackrabbitRepositoryAccessor.class) {
    if (!isCustomRegistered) {
     if (!ArrayUtils.isEmpty(customNodeTypeConfigs)) {
      NamespaceRegistry namespaceRegistry = jcrSession.getWorkspace().getNamespaceRegistry();

      for (NodeTypeConfig ndc : customNodeTypeConfigs) {
       if (ndc.getNamespacePrefix() != null && ndc.getNamespaceUri() != null) {
        // check whether the namespace prefix or uri already exist
        if (!ArrayUtils.contains(namespaceRegistry.getPrefixes(), ndc.getNamespacePrefix())
            || !ArrayUtils.contains(namespaceRegistry.getURIs(), ndc.getNamespaceUri())) {
         // register namespace
         namespaceRegistry.registerNamespace(ndc.getNamespacePrefix(),
                                             ndc.getNamespaceUri());
         if (LOG.isDebugEnabled()) {
          LOG.debug("Custom namespace prefix '" + ndc.getNamespacePrefix()
                    + "' has been registered to the custom uri '"
                    + ndc.getNamespaceUri() + "'");
         }
        }
       }

       if (ndc.getInputStream() != null) {
        registerNodeType(jcrSession, ndc.getInputStream());
       }
      }

      if (LOG.isDebugEnabled()) {
       LOG.debug("Register of " + customNodeTypeConfigs.length + " custom node types is ensured");
      }
     }

     isCustomRegistered = true;
    }
   }
  }

  return jcrSession;
 }

 /**
  * {@inheritDoc}
  */
 public void releaseSession()
 {
  Map<String, Session> workspace2Session = THREAD_SESSION.get();
  if (workspace2Session != null) {
   Collection<Session> sessions = workspace2Session.values();
   for (Session jcrSession : sessions) {
    if (jcrSession != null && jcrSession.isLive()) {
     if (LOG.isDebugEnabled()) {
      LOG.debug("==> Closing JCR Session for the current thread.");
     }

     jcrSession.logout();
    }
   }
  }

  THREAD_SESSION.set(null);
 }

 /**
  * {@inheritDoc}
  */
 public void releaseRepository()
 {
  // Jackrabbit specific
  if (repository instanceof JackrabbitRepository) {
   ((JackrabbitRepository) repository).shutdown();
  }

  repository = null;
 }
}
RepositoryAccessor should be accessible from application scope and can be instantiated during application startup (e.g. in ServletContextListener's contextInitialized() or in an JSF managed bean's method annotated with @PostConstruct). Well. Let's put all classes together! I would like to show typically steps to get an instance of JackrabbitRepositoryAccessor.
// create a google guice injector for the configuration module
Injector injector = Guice.createInjector(new DefaultConfigurationGuiceModule());

// create the factory instance to create a repository accessor instance
RepositoryAccessorFactory repositoryAccessorFactory = injector.getInstance(RepositoryAccessorFactory.class);

// create custom node type configurations from describing XML file and given namespace prefix / URI
NodeTypeConfig[] nodeTypeConfigs = new NodeTypeConfig[1];
nodeTypeConfigs[0] = NodeTypeConfig.getNodeTypeConfig("custom_node.xml", "xyz", "http://mysite.net/xyz");

// create an instance of repository accessor (parameter can be null if no custom node types are available)
repositoryAccessor = repositoryAccessorFactory.create(nodeTypeConfigs);

// method and field injection
injector.injectMembers(repositoryAccessor);

// start and initialize the content repository
repositoryAccessor.startRepository();
Now you can access both - Repository and JCR Session somewhere you want
javax.jcr.Repository repository = repositoryAccessor.getRepository();
javax.jcr.Session session = repositoryAccessor.getSession(workspaceName, viewerId);
Not forget to release repository when the application goes down (e.g. in ServletContextListener's contextDestroyed() or in an JSF managed bean's method annotated with @PreDestroy).
repositoryAccessor.releaseRepository();
repositoryAccessor = null;
That's all :-)

Setting up shared Jackrabbit content repository

There are several deployment models which are described in detail by Jackrabbit. For our purpose the "Model 2: Shared J2EE Resource" seems to be the best applicable. This way to deploy a repository is to make it visible as a resource to all the web applications that are running inside a servlet container by registering the repository as a resource adapter to the application server. The repository is started and stopped with the application server. I'm going to describe all necessary steps for the GlassFish application server:

Open up "Resources/JNDI/Custom Resources" in the GlassFish administration console
  • Put in a JNDI name "jcr/repository"
  • Put in a resource type "javax.jcr.Repository"
  • Put in the factory class "org.apache.jackrabbit.core.jndi.BindableRepositoryFactory"
  • Create the property configFilePath, pointing to a configuration XML file with an absolute path on the server, e.g. "c:/repository/repository.xml"
  • Create the property repHomeDir pointing to the absolute filesystem path for the repository, e.g. "c:/repository"
Copy Jackrabbit dependencies to GLASSFISH_HOME/glassfish/domains/domain1/lib/ext. These are in case of Jackrabbit 1.6.2:
 commons-collections-3.2.1.jar
 commons-io-1.4.jar
 commons-lang-2.4.jar
 concurrent-1.3.4.jar
 derby-10.2.1.6.jar
 jackrabbit-api-1.6.2.jar
 jackrabbit-core-1.6.2.jar
 jackrabbit-jcr-commons-1.6.2.jar
 jackrabbit-spi-1.6.2.jar
 jackrabbit-spi-commons-1.6.2.jar
 jackrabbit-text-extractors-1.6.2.jar
 jcr-1.0.jar
 log4j-1.2.15.jar
 lucene-core-2.4.1.jar
 mysql-connector-java-5.1.12-bin.jar (if datastore is MySQL)
 pdfbox-0.7.3.jar
 poi-3.2-FINAL.jar
 poi-scratchpad-3.2-FINAL.jar
To configure resource factory in Tomcat, add a <Resource> element to the <Context> in the file context.xml (global) or in the server.xml (for specific web app)
<Context ...>
  ...
  <Resource name="jcr/repository" auth="Container"
            type="javax.jcr.Repository"
            factory="org.apache.jackrabbit.core.jndi.BindableRepositoryFactory"
            configFilePath="c:/repository/repository.xml"
            repHomeDir="c:/repository"/>
  ...
</Context>
Jackrabbit dependencies have to be copied  to TOMCAT_HOME/lib. Now you can set up JNDI reference to jcr/repository and use JCR to manage content in the application server. The configured resource needs to be declared in the web.xml file:
<web-app>
    ...
    <resource-ref>  
    <description>JCR Repository</description>  
    <res-ref-name>jcr/repository</res-ref-name>  
    <res-type>javax.jcr.Repository</res-type>  
    <res-auth>Container</res-auth>  
    </resource-ref>
</web-app>
After all steps the repository will be created automatically by the registered above factory class.

Saturday, July 3, 2010

Output JSF messages with static and dynamic key parts or the power of JSTL c:set

At the beginning of my development with JSF I faced a problem when I tried to get messages if one part of key is static (fix) and the other one is dynamic. Let us suppose you have the following keys and messages in your property file
breadcrumb.overview=Overview
breadcrumb.new=New
breadcrumb.edit=Edit
....
and would like to output these messages in a loop with ui:repeat like this
<ui:repeat value="#{breadcrumbBean.items}" var="item">
   ...
   read and output messages with h:outputText
   ...
</ui:repeat>
Suppose the exported variable for the current item of the iteration "item" has a property "name" which references the dynamic part of key. In our case that are parts "overview", "new", "edit", ... The prefix "breadcrumb" is in contrast fix. If you try now
<h:outputText value="#{msgBundle['breadcrumb.' + item.name]}" />
or
<h:outputText value="#{msgBundle['breadcrumb.'item.name]}" />
or
<h:outputText value="#{msgBundle[breadcrumb.item.name]}" />
nothing works. What is the right syntax? Here is a solution:
<c:set var="key" value="breadcrumb.#{item.name}" />
<h:outputText value="#{msgBundle[key]}" />
JSTL c:set buffers at first the entire key in a scoped variable "key", so that h:outputText can access it later.

By the way, c:set is very helpful in JSF 2 if you want to assign a dynamically Id to any JSF compoment. Component Id can not be set dynamically by EL. It's a static string. You can not write e.g.
<h:panelGroup id="#{myId}" layout="block"/>
What is a possible solution? Use the implicit object "component" to point enclosed component in c:set "target" atribute. An example:
<h:panelGroup layout="block">
    <c:set target="#{component}" property="id" value="#{idPrefix}panelGroup"/>
</h:panelGroup>
That's all!

Monday, June 28, 2010

Submitted value, local value, model value ...

There are 4 places where the value is stored during JSF lifecycle.
  1. The typed value comes with a request and is placed into request parameter map.
  2. Then the "submitted value" of component will be set.
  3. The "submitted value" is checked against the converter and only if it's OK the "local value" is updated and the "submitted value" is cleaned up.
  4. Then, only if validation und update phases are passed successfully, the "model value" (e.g. #{foo.bar}) is updated and the "local value" is cleaned up.

Assume, the user enters a wrong date. The page after reload should show the value user entered, but not the old one from the model. Therefore, the algorithmus in the render response phase is the following:
- if a submitted value is not empty, grab it and show.
- otherwise, if a local value is not null, show it.
- otherwise, call a getter of the model value and show it.

Unfortunately, but some JSF component libraries forget this fact. I use PrimeFaces - a great, JSF 2 "ready", rapidly evolving component library and more. I didn't check all components, but at least two components don't consider submitted values in the current release 1.0.2 / 2.0.2. That are: PickList and Calendar. I had to write a customizing code during markup encoding because it's important e.g. for failed validation or wizard like functionality.

PickListRenderer
DualListModel model;
Object submittedValue = pickList.getSubmittedValue();
if (submittedValue != null) {
    model = (DualListModel) getConvertedValue(facesContext, pickList, submittedValue);
} else {
    model = (DualListModel) pickList.getValue();
}
CalendarRenderer
String valueAsString;
if (calendar.getSubmittedValue() != null) {
   valueAsString = (String) calendar.getSubmittedValue();
} else {
   valueAsString = CalendarUtils.getValueAsString(facesContext, calendar);
}

How to configure Apache Jackrabbit for binary content search?

The configuration file repository.xml is described in detail by Apache Jackrabbit. The section for workspace and versioning configuration must be extended to support binary content search as follows:
<SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
    <param name="path" value="${wsp.home}/index"/>
    <param name="extractorPoolSize" value="2"/>
    <param name="supportHighlighting" value="true"/>
    <param name="textFilterClasses"
      value="org.apache.jackrabbit.extractor.PlainTextExtractor,
      org.apache.jackrabbit.extractor.MsWordTextExtractor,
      org.apache.jackrabbit.extractor.MsExcelTextExtractor,
      org.apache.jackrabbit.extractor.MsPowerPointTextExtractor,
      org.apache.jackrabbit.extractor.PdfTextExtractor,
      org.apache.jackrabbit.extractor.OpenOfficeTextExtractor,
      org.apache.jackrabbit.extractor.RTFTextExtractor,
      org.apache.jackrabbit.extractor.HTMLTextExtractor,
      org.apache.jackrabbit.extractor.XMLTextExtractor"/>
</SearchIndex>
Content of following document types can be full text searchable with this configuration
  • Plain Text
  • MS Word
  • MS Excel
  • MS Powerpoint
  • PDF
  • OpenOffice
  • RTF
  • HTML
  • XML
There are two points to be considered: Content repository needs some time after document adding to parse documents content and extract needed informations. In my tests I had to wait ca. 7 sek.
// add document and save changes
....

// sleep 7 sek. to allow the content of document be indexed
try {
    Thread.sleep(7000);
} catch (InterruptedException e) {
    ;
}

// do full text search
....
The second point is related to the configuration of node types. The full text search works if you use quite normally node type nt:file which contains a sub node jcr:content of type nt:resource. If you use custom node types you must ensure that the node type describing binary content has at least two properties: jcr:data (content is stored here) and jcr:mimeType. The second property for mime type is very important. Without the mime type there isn't text extraction (consequential, isn't it?). Here is an example in XML notation:
<nodeType name="cssns:resource"
          isMixin="false"
          hasOrderableChildNodes="false"
          primaryItemName="jcr:data">
    <supertypes>
        <supertype>nt:base</supertype>
        <supertype>mix:referenceable</supertype>
    </supertypes>
    <propertyDefinition name="jcr:mimeType"
                        requiredType="String"
                        autoCreated="false"
                        mandatory="true"
                        onParentVersion="COPY"
                        protected="false"
                        multiple="false">
    </propertyDefinition>
    <propertyDefinition name="jcr:data"
                        requiredType="Binary"
                        autoCreated="false"
                        mandatory="true"
                        onParentVersion="COPY"
                        protected="false"
                        multiple="false">
    </propertyDefinition>
</nodeType>

<nodeType name="cssns:file"
          isMixin="false"
          hasOrderableChildNodes="false"
          primaryItemName="jcr:content">
    <supertypes>
        <supertype>mix:versionable</supertype>
        <supertype>cssns:hierarchyNode</supertype>
    </supertypes>
    <propertyDefinition name="cssns:size"
                        requiredType="Long"
                        autoCreated="true"
                        mandatory="true"
                        onParentVersion="COPY"
                        protected="false"
                        multiple="false">
        <defaultValues>
            <defaultValue>-1</defaultValue>
        </defaultValues>
    <childNodeDefinition name="jcr:content"
                          defaultPrimaryType=""
                         autoCreated="false"
                         mandatory="true"
                          onParentVersion="COPY"
                          protected="false"
                          sameNameSiblings="false">
     <requiredPrimaryTypes>
         <requiredPrimaryType>cssns:resource</requiredPrimaryType>
     </requiredPrimaryTypes>
    </childNodeDefinition>
</nodeType>