Monday, November 19, 2007

Spring + Hibernate Usefuls

The below custom BaseDAOHibernate class should reduce most of the common DAO related coding. The search and searchAdvanced will be used to wrap the select query related method calls.

package com.xmp.web2.dao;


public class BaseDAOHibernate extends HibernateDaoSupport implements DAO {

private static final Log LOGGER = LogFactory.getLog(BaseDAOHibernate.class);

protected DataSource dataSource;

public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}

public void saveObject(final Object object) {
getHibernateTemplate().saveOrUpdate(object);
}

public Object getObject(final Class clazz, final Serializable id) {
Object object = getHibernateTemplate().get(clazz, id);

return object;
}

public List getObjects(final Class clazz) {
return getHibernateTemplate().loadAll(clazz);
}

public void removeObject(final Class clazz, final Serializable id) {
getHibernateTemplate().delete(getObject(clazz, id));
}

public List findObject(final Class clazz, final String columnName, final String articleID) {
return getHibernateTemplate().find("from " + clazz.getName() + " tableAlias where tableAlias." + columnName + " in ( " + articleID + " )");
}

/**
* Accepts a HQL and ordered map of parameters.
*
* @param query
* @param parameters
* @return
*/
public List search(String query, LinkedHashMap parameters) throws xmpWebException {
boolean keepSessionOpen=false;
return search(query, parameters, keepSessionOpen);
}

/**
* Accepts a HQL and ordered map of parameters.
* The caller of this method should explicitly close the session.
* @param query
* @param parameters
* @parm keepSessionOpen -boolean to instruct if the session should be closed or not.
* @return
*/
protected List search(String query, LinkedHashMap parameters, boolean keepSessionOpen) throws xmpWebException {
List objList = null;
Session session = null;
try {
 if (LOGGER.isInfoEnabled()) {
  LOGGER.info(new StringBuilder().append("Searching ( query = ").append(query).append(" ,parameters =").append(parameters).append(")").toString());
 }
 session = getHibernateTemplate().getSessionFactory().openSession();
 Query queryObj = session.createQuery(query);
  queryObj.setCacheable(true);

 if (parameters != null) {
  for (Iterator iter = parameters.keySet().iterator(); iter.hasNext();) {
   String key = (String) iter.next();
   Object value = parameters.get(key);
   queryObj.setParameter(key, value);
  }
 }
 objList = queryObj.list();

} catch (Exception e) {
 String msg = new StringBuilder("Search failed ( query = ").append("Searching ( query = ").append(query).append(" ,parameters =").append(parameters).append(")").toString();
 LOGGER.error(msg, e);
 throw new xmpWebException(msg, e);
} finally {
 if (!keepSessionOpen) {
  if (session != null && session.isOpen())
   session.close();
 }
}
return objList;
}

/**
* Accepts a HQL and ordered map of parameters. Useful with pagination as it
* accepts pageNo and pagesize.
*
* @param query
* @param parameters
* @param pageNo - The starting page number
* @param pageSize - The max record size.
* @return
*/
protected List searchAdvanced(final String query, final LinkedHashMap parameters, final int pageNo, final int pageSize) throws xmpWebException {

boolean keepSessionOpen = false;
return searchAdvanced(query, parameters, pageNo, pageSize, keepSessionOpen);
}
/**
* Accepts a HQL and ordered map of parameters. Useful with pagination as it
* accepts pageNo and pagesize.
*
* @param query
* @param parameters
* @param pageNo - The starting page number
* @param pageSize - The max record size.
* @param keepSessionOpen - boolean to identify if session needs to be kept open or not.
* @return
*/
protected List searchAdvanced(final String query, final LinkedHashMap parameters, final int pageNo, final int pageSize, boolean keepSessionOpen) throws xmpWebException {
if (LOGGER.isInfoEnabled()) {
 LOGGER.info(new StringBuilder("Searching with ( query = ").append(query).append(" ,parameters =").append(parameters).append(" ,pageNo =").append(pageNo).append(" ,pageSize= ").append(
   pageSize).append(" )").toString());
}
Session session = null;
try {
 session = getHibernateTemplate().getSessionFactory().openSession();
 Query queryObject = session.createQuery(query);
 // queryObj.setCacheable(true);
 if (parameters != null) {
  for (Iterator iter = parameters.keySet().iterator(); iter.hasNext();) {
   String key = (String) iter.next();
   Object value = parameters.get(key);
   queryObject.setParameter(key, value);
  }
 }
 queryObject.setMaxResults((pageSize < msg =" new" query = ").append(query).append(" parameters =").append(parameters).append(" pageno =").append(pageNo).append(" pagesize= ").append(      pageSize).append(">                                                                                             
------------------------------------------------------------------------------------------------------------------------------------------------------------------------ The above should solve most of the basic coding effort in DAO layer which we do 90% of the time. Spring+Hibernate Junit testing: Spring framework provides a nifty base class (AbstractTransactionalDataSourceSpringContextTests) that provides automatic transaction rollback, exposing a JDBC template to interact with the DB and auto wiring of beans.

Lets take a simple DAO class that save a User object to the database:

public class UserDaoImpl extends HibernateDaoSupport implements UserDao {
public void save(User user) {
getHibernateTemplate().save(user);
}
}
Now in order to test this you would write a test class as below extending from AbstractTransactionalDataSourceSpringContextTests class.
public class UserDaoTest extends AbstractTransactionalDataSourceSpringContextTests {
private UserDao userDao;
private SessionFactory sessionFactory = null;

protected String[] getConfigLocations() {
return new String[]{"test-spring-config.xml"};
}

public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}

public void setSessionFactory(SessionFactory sessionFactory) {
this.sessionFactory = sessionFactory;
}

/**
* Test the save method
*
*/
public void testSave(){
String query = "select count(*) from user where first_name = 'Firstname'";
int count = jdbcTemplate.queryForInt(query);
assertEquals("A user already exists in the DB", 0, count);

User user = new User();
user.setFirstName("Firstname");

userDao.saveUser(user);

// flush the session so we can get the record using JDBC template
SessionFactoryUtils.getSession(sessionFactory, false).flush();

count = jdbcTemplate.queryForInt(query);
assertEquals("User was not found in the DB", 1, count);
}
}
The test class has to implement the protected String[] getConfigLocations() method from the base class and return a String array of Spring config files which will be used to initialize the Spring context. UserDao and SessionFactory properties are defined with the setter methods and the base class will take care of injecting them automatically from the Spring context. Auto wiring will not work if there are multiple objects implementing the same interface. In such a case you can remove the setter method and retrieve the object using the exposed applicationContext as below.
   /**
* Overridden method from base class which gets called automatically
*/
protected void onSetUpBeforeTransaction() throws Exception {
super.onSetUpBeforeTransaction();
userDao = (UserDao) applicationContext.getBean("userDao");
}
The base class also exposes a JDBC template object (jdbcTemplate) that can be used to query data or setup test data in the database. Note that you need to have a data source and a transaction manager defined in your Spring config in order to use the AbstractTransactionalDataSourceSpringContextTests base class. The data source defined in the config file will be bound to the exposed JDBC template. In the testSave method first we verify there is no record in the User table where first name equals to 'Firstname' using the jdbc template object. Then we call the save method on the UserDao passing it a User object. Now we simple verify there is a record in the table where first name equals to 'Firstname'. Before running the query we flush the current Hibernate session to make sure jdbcTemplate can see the newly added record. Thats it and when the testSave method exits the current transaction will be rolled back and the record inserted to the User table will not be saved. This is great as your test database will always be at a know state at the start and end of a test method. The spring config file will like below (test-spring-config.xml) :

<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">

<beans>
<bean name="userDao" class="com.dao.UserDaoImpl">
<property name="sessionFactory">
   <ref bean="sessionFactory"/>
</property>
</bean>

<bean id="sessionFactory" class="org.springframework.orm.hibernate3.LocalSessionFactoryBean">
<property name="dataSource">
   <ref bean="dataSource"/>
</property>
<property name="mappingResources">
   <list>
       <value>hibernates/User.hbm.xml</value>
   </list>
</property>
<property name="hibernateProperties">
   <props>
       <prop key="hibernate.query.factory_class">org.hibernate.hql.classic.ClassicQueryTranslatorFactory</prop>
       <prop key="hibernate.dialect">org.hibernate.dialect.OracleDialect</prop>
   </props>
</property>
</bean>


<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="oracle.jdbc.driver.OracleDriver"/>
<property name="url" value="jdbc:oracle:thin:@localhost:1521:ORCL" />
<property name="username" value="test" />
<property name="password" value="test" />
</bean>


<bean id="txManager" class="org.springframework.orm.hibernate3.HibernateTransactionManager">
<property name="sessionFactory"><ref local="sessionFactory"/></property>
</bean>

</beans>
Useful stuff: HQL many to many query for retreival from UserTask ut where :journal in elements(ut.journals)

Wednesday, November 14, 2007

Single Sign on - OpenSSO

Single Sign on using OpenSSO.

OpenSSO was earlier called as access manager.
OpenFM is openSSO + federation (Cross domain support?)

It was a herculian effort to get it going with openfm/OpenSSO.

  1. Access Manager(opensso/openfm) configuration:
Mainly it seems to be having bugs in the user interface of the webapp it provides.
  • First thing I came across when deploying the openfm.war on linux was as below:

  • -First time when we go to http://localhost:8080/openfm and try to
    configure the openfm, it gives a error with no stack trace nothing . It
    misleads you with a path to log file which you can find in their(sso
    developers) dreams.GRRRR.
  • Had to search around
    in Red hat linux Enterprise edition 5. Later figured that it creates a
    folder with name @BASEDIR@ under tomcat/bin/... Who can imagine a
    cryptic folder for logging sso errors that too under tomcat/bin/...
    Wasted almost 1.5 days on that ....
  • In another version of linux, the above didn't work out. It was in tomcat logs catalina.out. Lucky!!
  • The
    problem usually tends to be due to wrong JDK. The Sun JCE comes as
    default with Sun JDK but not with IBM JDK. The Sun JCE is used for
    encryption of password by open SSO.

  • Another important thing. First time you setup access manager you should
    be careful. Next time, if you try setting it up(by deploying new
    openfm.war) , it complains. Under windows u can simply delete the
    access manager folder created during installation (default is C:\Doc and settings\user name\)...
  • Under linux it should be somewhere under /home/ by default. Search using locate access manager. The custom path would be the one which you setup openSSO using Configurator.jsp.
  • If
    you mess up access manager by configuring authentication chain or data
    store, the work around is use the default module=DataStore as URL parameter eg: http://localhost:8080/openfm/UI/Login?module=DataStore
Then you can login as amAdmin
  • By
    default the openfm ships with set of authentication plugin like JDBC,
    LDAP based , etc .Our requirement needed to compare the user entered
    auth password with MD5 encrypted password. Hence had to build a custom
    authentication plugin. Luckily sun provide service provider
    interface(SPI).Implementing this was a major effort as there is hardly
    any documentation or forum talking about it.Took a short cut by
    extending the sun provided com.sun.identity.authentication.modules.jdbc.JDBC.java
    and over riding transform() method. It works smooth

To add a custom authentication plugin, follow the below steps:
  1. Move the custom

    authentication jar which you wrote (opensso_xmp_plug_v1.0.1.jar) into
    the ~/openfm/WEB-INF/lib folder.All the related property files should
    be on class path.


  1. Copy the custom
    Authentication JDBC configuration file. amAuthxmpJDBC.xml into ~/openfm/WEB-INF/classes
    folder.Refer amAuthJDBC.xml in the same folder for creating a similar one for your custom auth module.

  1. Register the module in
    serviceNames.properties abailable under openfm/WEB-INF/classes to have
    amAuthXMPJDBC.xml. (Add amAuthxmpJDBC.xml at the end)

  1. Copy XMPJDBC.xml into ~/openfm/config/auth/default
  2. Restart tomcat.
  3. Login to Access Manager, Goto
    Configuration
    -- Authentication --Core
  4. Enter com.xmp.security.plugin.XMPJDBC as New Value and click on Add to configure
    the new service.
  5. Go to Access Control and
    select the realm (opensso). Click on Authentication &gt; Module Instances
    and Add the previously configured XMPJDBC module to the authentication
    chain as shown below: Save the information.
  6. Now the Login of opensso will use xmpJDBC module as default for
    authentication. If you want to login with the amAdmin user,
    module=DataStore need to be added to login URL (like
    http://localhost/openfm?module=DataStore)
  7. Login with a valid userId and password (sample xello@xello.com/xello)
    The user is taken to the successful login page.

NOTE:
  • Once a user is logged in successfully, the access manager by default
    looks for the user's profile through Id repo. This is the default
    behaviour. This can be over ridden by setting the property in
    realm(opensso) -- Authentication -- Advanced (look profile) to ignored.

  • Using custom com.sun.identity.agents.filter.SSOTaskHandler class, we can
    insert a session attribute which is used by the SSO agent/application
    for auto login (discussed later , for now agent is like a client to
    open sso). The sample code is below:
public class SSOTaskHandler extends AmFilterTaskHandler implements ISSOTaskHandler
{...
public AmFilterResult process(AmFilterRequestContext amfilterrequestcontext) throws AgentException
{

.........
amfilterrequestcontext.getHttpServletRequest().getSession().setAttribute("SSO_VALIDATION_RESULT",ssovalidationresult);
.............
}
........
}



TODO: J2EE agent
The agent J2ee 007 would be hiding in the web application root web.xml(as AmAgentFilter) to provide secured access. ;)
AMAgent.properties is where the whole good behaviors of the badly behaving J2ee agent is configured. This is generated by amadmin tool and updated later. Check it below for only the important parameters to manually update
----------------------------------------------------------------------------------------------------------

#
# CDSSO PROCESSING PROPERTIES
com.sun.identity.agents.config.cdsso.enable = true
com.sun.identity.agents.config.cdsso.redirect.uri = /agentapp/sunwCDSSORedirectURI
com.sun.identity.agents.config.cdsso.cdcservlet.url[0] = http://172.20.41.39:6060/openfm/cdcservlet
com.sun.identity.agents.config.cdsso.clock.skew = 0
com.sun.identity.agents.config.cdsso.trusted.id.provider[0] = http://172.20.41.39:6060/openfm/cdcservlet

#
# LOGOUT PROCESSING PROPERTIES
com.sun.identity.agents.config.logout.application.handler[] =
com.sun.identity.agents.config.logout.uri[DefaultWebApp] =/web/xmpXMS/logout
com.sun.identity.agents.config.logout.request.param[] =
com.sun.identity.agents.config.logout.introspect.enabled = false
com.sun.identity.agents.config.logout.entry.uri[DefaultWebApp] =/web/xmpXMS/home
#
# NOT-ENFORCED URI PROCESSING PROPERTIES
# - notenforced.uri: A LIST of URIs for which protection is not enforced
# by the Agent.
# - notenforced.uri.invert: A flag that specifies if the list of URIs
# specified by the property notenforced.uri should be inverted. When
# set to true, it indicates that the URIs specified should be enforced
# and all other URIs should be not enforced by the Agent. Entries in
# this list can have wild card character '*'.
# Example of notenforced.uri:
# com.sun.identity.agents.config.notenforced.uri[0]=*.gif
# com.sun.identity.agents.config.notenforced.uri[1]=/public/*
# com.sun.identity.agents.config.notenforced.uri[2]=/images/*
#
com.sun.identity.agents.config.notenforced.uri[0] =
com.sun.identity.agents.config.notenforced.uri.invert = false
com.sun.identity.agents.config.notenforced.uri.cache.enable = true
com.sun.identity.agents.config.notenforced.uri.cache.size = 1000

#
# DEBUG SERVICE PROPERTIES
# - com.iplanet.services.debug.level: Specifies the debug level to be used.
# The value is one of: off, error, warning, message. ******** Funny thing, debug is missing but it actually is very useful for developers********
com.iplanet.services.debug.level=debug
--------------------------------------------------------------------------------------------------------------

TODO: Web Agent







Powered by ScribeFire.