Friday, 3 August 2012

Server Side Tracing Microsoft SQL Server Events

Consider a situation where you must find out about user error messages on your production server. You can use profiler, but overhead is not welcomed. Solution is server side tracing whose overhead is significantly reduced (in fact one such trace is running by default). Server side tracing can collect the same data as profiler can.

Example of creating server-side trace can be found here. So here is the script that enables tracing "User Error Message" events (script has been slightly modified):

/*****************************************************************************
This script starts server-side trace.
******************************************************************************/
-- Declare variables 
DECLARE @rc INT 
DECLARE @TraceID INT 
DECLARE @maxFileSize bigint 
DECLARE @fileName NVARCHAR(128) 
DECLARE @on bit 

-- Set values 
SET @maxFileSize = 5 -- in MB 
SET @fileName = N'D:\UserErrorMessageTrace' -- location must be writeable for SQL server
SET @on = 1 

-- Create trace 
EXEC @rc = sp_trace_create @TraceID output, 0, @fileName, @maxFileSize, NULL  

-- If error end process 
IF (@rc != 0) 
 SELECT 'Could not create trace. Error code: ' + CAST(@rc as varchar(10))
ELSE BEGIN
 -- Declare event ID to trace
 DECLARE @EventId int = 162 -- 'User Error Message'

 -- Set columns to collect
 EXEC sp_trace_setevent @TraceID, @EventId,  1, @on 
 EXEC sp_trace_setevent @TraceID, @EventId,  4, @on 
 EXEC sp_trace_setevent @TraceID, @EventId,  6, @on 
 EXEC sp_trace_setevent @TraceID, @EventId,  7, @on 
 EXEC sp_trace_setevent @TraceID, @EventId,  8, @on 
 EXEC sp_trace_setevent @TraceID, @EventId, 11, @on 
 EXEC sp_trace_setevent @TraceID, @EventId, 12, @on 
 EXEC sp_trace_setevent @TraceID, @EventId, 14, @on 
 EXEC sp_trace_setevent @TraceID, @EventId, 20, @on 
 EXEC sp_trace_setevent @TraceID, @EventId, 27, @on 
 EXEC sp_trace_setevent @TraceID, @EventId, 30, @on 
 EXEC sp_trace_setevent @TraceID, @EventId, 31, @on 
 EXEC sp_trace_setevent @TraceID, @EventId, 35, @on 
 EXEC sp_trace_setevent @TraceID, @EventId, 51, @on 
 EXEC sp_trace_setevent @TraceID, @EventId, 64, @on 

 -- Set Filters 
 EXEC sp_trace_setfilter @TraceID, 31, 0, 1, 5701 -- exclude language change messages
 EXEC sp_trace_setfilter @TraceID, 31, 0, 1, 5703 -- exclude context change messages

 -- Start the trace 
 EXEC sp_trace_setstatus @TraceID, 1 
   
 -- Report success
 SELECT 'Started trace with ID = ' + cast(@TraceID as varchar(10))
END

Trace events and columns

So... which events can you trace and what columns those events support? There are some system views that contain all required info. All traceable events can be found by querying sys.trace_events view, columns are stored in sys.trace_columns view and event column configuration is in sys.trace_event_bindings view. In order not to write all those sp_trace_setevent one by one, use this query that returns all columns for particular event and corresponding EXEC statement (replace "164" with event ID of your interest):

SELECT 'EXEC sp_trace_setevent @TraceID, @EventId, ' 
  + cast(c.trace_column_id AS VARCHAR(10)) 
  + ', @on'
 ,*
FROM sys.trace_event_bindings AS b
INNER JOIN sys.trace_columns AS c ON c.trace_column_id = b.trace_column_id
WHERE b.trace_event_id = 164

Check active traces

In order to check currently active traces, you can use sys.fn_trace_getinfo system function (refer to the "Tables Returned" section for explanation of returned values). Enabled columns can be retrieved using fn_trace_geteventinfo system function. In order to retrieve more human-understandable data (with column names), execute this query:

SELECT DISTINCT c.*
FROM fn_trace_geteventinfo(1) AS i
INNER JOIN sys.trace_columns AS c ON c.trace_column_id = i.columnid

Start/Stop/Close trace and load data

Manage trace lifecycle with sp_trace_setstatus system function and load trace data using sys.fn_trace_gettable function. See examples here

Friday, 27 July 2012

Configure Microsoft SQL Server to Report Events to E-mail

For this I had to combine 3 blog articles:

Step 1: Configure Database Mail

Follow the instructions here: http://www.mssqltips.com/sqlservertip/1100/setting-up-database-mail-for-sql-2005/

Summary of steps (with minor variations):

  1. Select "Configure Database Mail" by left clicking on "Database Mail"
  2. Next
  3. "Manage Database Mail accounts and profiles"... Next
  4. Create a new profile... Next
  5. Configure Profile:
    1. Profile name: "SQLAlerts"
    2. Add..
      1. New Account
        1. Account name: "SQLAlerts"
        2. E-mail address: mysqlserver@example.com (this will be shown in "sender" e-mail field)
        3. Display Name: My SQL Server (this will be shown as display name in "sender" e-mail field)
        4. Server Name: smtp.example.com (outgoing SMTP server)
        5. ... configure authentication
        6. OK
      2. OK
    3. Next
    4. Finish
    5. Close

Step 2: Create new Operator and Alert

Corresponding blog post: http://www.mssqltips.com/sqlservertip/1523/how-to-setup-sql-server-alerts-and-email-operator-notifications/

Summary of steps:

  1. Under "SQL Server Agent" right click on Operators and select "New Operator..."
    1. Name: "DBA"
    2. E-mail name: your-email-here@example.com
    3. OK
  2. Right click on "SQL Server Agent" and choose "Properties"
    1. Choose "Alert System" on the left hand side
    2. Check "Enable mail profile"
    3. If you have more than one mail profile configured, then choose the desired one
    4. OK
  3. Right click on "SQL Server Agent" and choose "Restart"
  4. Under "SQL Server Agent" right click on "Alerts" and select "New Alert..."
    1. Under "General" tab:
      1. Name: "14 - Insufficient Permission"
      2. Severity: "14 - Insufficient Permission"
    2. Under "Response" tab:
      1. Check "Notify operators"
      2. Check E-mail options of those operators that should be notified about an error
    3. Under "Options" tab
      1. Check "E-mail" checkbox in order to include error description in the mail text

This is the point where your alert sending has been configured, and you're wondering "Why I'm not getting error reports about insufficient permissions?!" Here comes the third step:

Step 3: Alter messages

Corresponding blog article: http://www.simple-talk.com/sql/database-administration/sql-server-alerts-soup-to-nuts/

In order to SQL Server Agent detect errors, they must be written to Application Event Log, but most of them are not. That can be changed using "sp_altermessage" database engine stored procedure. For errors that you would like to receive, execute this query:

sp_altermessage 229, 'WITH_LOG', 'true';

This particular query will enable reporting messages like "The SELECT permission was denied on the object 'mytable', database 'bydb', schema 'dbo'" to application log (and with that - e-mail).

You can explore messages that you could be interested in with this query:

The SELECT permission was denied on the object 'prkad', database 'person', schema 'dbo'

Thursday, 3 May 2012

Binding spring form using bean with map attribute retrieved using hibernate

Another one exception bit me today. Spring forms nicely support maps as bean properties using 'mapProperty[key].mapElementProperty' paths, but each time I tried to submit form, exception was thrown (see below). As it turns out, problem was caused by hibernate which returned bean proxy. When setting map element property values after form submit, Spring tries to find out what type of elements map contains using generics information (... using some kind of magic), but in the end it didn't work. Not sure, but I suppose it could be because javassist can't handle generics because of type erasure. Solution was simple - modify DAO method which returned editable object (getById(id)) so that it would not return a hibernate-managed object, but a cloned object. As soon as those changes were applied, everything started to work.
java.lang.NullPointerException
 at org.springframework.beans.BeanWrapperImpl.createDefaultPropertyValue(BeanWrapperImpl.java:620)
 at org.springframework.beans.BeanWrapperImpl.setDefaultValue(BeanWrapperImpl.java:614)
 at org.springframework.beans.BeanWrapperImpl.getNestedBeanWrapper(BeanWrapperImpl.java:579)
 at org.springframework.beans.BeanWrapperImpl.getBeanWrapperForPropertyPath(BeanWrapperImpl.java:553)
 at org.springframework.beans.BeanWrapperImpl.setPropertyValue(BeanWrapperImpl.java:914)
 at org.springframework.beans.AbstractPropertyAccessor.setPropertyValues(AbstractPropertyAccessor.java:76)
 at org.springframework.validation.DataBinder.applyPropertyValues(DataBinder.java:692)
 at org.springframework.validation.DataBinder.doBind(DataBinder.java:588)
 at org.springframework.web.bind.WebDataBinder.doBind(WebDataBinder.java:191)
 at org.springframework.web.portlet.bind.PortletRequestDataBinder.bind(PortletRequestDataBinder.java:113)
 at org.springframework.web.portlet.mvc.annotation.AnnotationMethodHandlerAdapter$PortletHandlerMethodInvoker.doBind(AnnotationMethodHandlerAdapter.java:570)
 at org.springframework.web.bind.annotation.support.HandlerMethodInvoker.doBind(HandlerMethodInvoker.java:813)
 at org.springframework.web.bind.annotation.support.HandlerMethodInvoker.resolveHandlerArguments(HandlerMethodInvoker.java:367)
 at org.springframework.web.bind.annotation.support.HandlerMethodInvoker.invokeHandlerMethod(HandlerMethodInvoker.java:171)
 at org.springframework.web.portlet.mvc.annotation.AnnotationMethodHandlerAdapter.invokeHandlerMethod(AnnotationMethodHandlerAdapter.java:362)
 at org.springframework.web.portlet.mvc.annotation.AnnotationMethodHandlerAdapter.doHandle(AnnotationMethodHandlerAdapter.java:349)
 at org.springframework.web.portlet.mvc.annotation.AnnotationMethodHandlerAdapter.handleAction(AnnotationMethodHandlerAdapter.java:283)
 at org.springframework.web.portlet.DispatcherPortlet.doActionService(DispatcherPortlet.java:641)
 at org.springframework.web.portlet.FrameworkPortlet.processRequest(FrameworkPortlet.java:519)
 at org.springframework.web.portlet.FrameworkPortlet.processAction(FrameworkPortlet.java:460)
 at org.jasig.portal.portlet.container.FilterChainImpl.doFilter(FilterChainImpl.java:130)
 at org.jasig.portal.portlet.container.FilterChainImpl.processFilter(FilterChainImpl.java:92)
 at org.jasig.portal.portlet.container.FilterManagerImpl.processFilter(FilterManagerImpl.java:119)
 at org.apache.pluto.container.driver.PortletServlet.dispatch(PortletServlet.java:359)
 at org.apache.pluto.container.driver.PortletServlet.doPost(PortletServlet.java:267)
 at javax.servlet.http.HttpServlet.service(HttpServlet.java:637)
 at javax.servlet.http.HttpServlet.service(HttpServlet.java:717)
 at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:290)
 at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
 at org.apache.catalina.core.ApplicationDispatcher.invoke(ApplicationDispatcher.java:646)
 at org.apache.catalina.core.ApplicationDispatcher.doInclude(ApplicationDispatcher.java:551)
 at org.apache.catalina.core.ApplicationDispatcher.include(ApplicationDispatcher.java:488)
 at org.apache.pluto.driver.container.DefaultPortletInvokerService.invoke(DefaultPortletInvokerService.java:233)
 at org.apache.pluto.driver.container.DefaultPortletInvokerService.action(DefaultPortletInvokerService.java:101)
 at sun.reflect.GeneratedMethodAccessor295.invoke(Unknown Source)
 at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
 at java.lang.reflect.Method.invoke(Method.java:597)
 at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:318)
 at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:183)
 at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:150)
 at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:90)
 at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:172)
 at org.springframework.aop.aspectj.MethodInvocationProceedingJoinPoint.proceed(MethodInvocationProceedingJoinPoint.java:80)
 at org.jasig.portal.portlet.dao.jpa.ThreadContextClassLoaderAspect.doThreadContextClassLoaderUpdate(ThreadContextClassLoaderAspect.java:56)
 at sun.reflect.GeneratedMethodAccessor105.invoke(Unknown Source)
 at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
 at java.lang.reflect.Method.invoke(Method.java:597)
 at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethodWithGivenArgs(AbstractAspectJAdvice.java:621)
 at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethod(AbstractAspectJAdvice.java:610)
 at org.springframework.aop.aspectj.AspectJAroundAdvice.invoke(AspectJAroundAdvice.java:65)
 at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:172)
 at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:202)
 at $Proxy207.action(Unknown Source)
 at org.apache.pluto.container.impl.PortletContainerImpl.doAction(PortletContainerImpl.java:251)
 at org.jasig.portal.portlet.rendering.PortletRendererImpl.doAction(PortletRendererImpl.java:165)
 at org.jasig.portal.portlet.rendering.worker.PortletActionExecutionWorker.callInternal(PortletActionExecutionWorker.java:46)
 at org.jasig.portal.portlet.rendering.worker.PortletActionExecutionWorker.callInternal(PortletActionExecutionWorker.java:31)
 at org.jasig.portal.portlet.rendering.worker.PortletExecutionWorker$1.call(PortletExecutionWorker.java:145)
 at java.util.concurrent.FutureTask$Sync.innerRun(FutureTask.java:303)
 at java.util.concurrent.FutureTask.run(FutureTask.java:138)
 at java.util.concurrent.ThreadPoolExecutor$Worker.runTask(ThreadPoolExecutor.java:886)
 at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:908)
 at java.lang.Thread.run(Thread.java:662)

Wednesday, 28 March 2012

Centring DIV elements

Here's a HTML structure and required CSS:

<div id="container">
  <div class="centered">This is centered div</div>
</div>
#container { text-align:center;}
.centered { margin:0 auto; width:80%; } /* Set the width according to your needs */

Applying "margin: 0 auto;" would be sufficient in a world without IE. In order to get centred div element in IE, you have to set "text-align: center;" for parent element. That parent element could be another div (just like in example), body element (then style definition would be "body {text-align: center}") or anything else. There's also a lot more detailed explanation of this approach here, but I'm too lazy to read the whole article :(.

Already knew this? Good for you! I did have to spend some time reading discussions and long descriptions about this, but didn't find a single concise "here's a solution - copy, paste, and enjoy" answer.

Are there any drawbacks of this method? Haven't found such yet. If you know something that I'm not aware of, please, let me know this in comments.

Are there any alternatives? Yes:

  • I've seen (and used) alternatives where DIV element is being included into a table which is centred. But it is not recommended approach since page structure should be built using DIVs. I know - it's a challenging task, but that's an idea I've got from different discussions.
  • Also I've found a discussion where a fixed-width DIV gets positioned at the centre of parent element and then a negative margin is applied (haven't tried this one, but looks promising). Here's a CSS:
    .centered{
      width: 500px;
      position: relative;
      left: 50%;
      margin-left: -250px; /* half the width of the div */
    }

Wednesday, 21 March 2012

Bytecode instrumentation for Hibernate using Maven

Lazy property loading for hibernate is nice when you're using complex formulas for calculating values. But it requires bytecode instrumentation or otherwise hibernate will ignore 'lazy="true"' on property element and load the value eagerly. Here's the snippet of my pom.xml that does the instrumentation:
    <build>
        <plugins>
            ...
            <!-- Bytecode insturmentation in order to enable lazy property loading by Hibernate. -->
            <plugin>
                <artifactId>maven-antrun-plugin</artifactId>
                <version>1.3</version>
                <executions>
                    <execution>
                        <id>Instrument domain classes</id>
                        <configuration>
                            <tasks>
                                <taskdef name="instrument" classname="org.hibernate.tool.instrument.javassist.InstrumentTask">
                                    <classpath>
                                        <path refid="maven.dependency.classpath" />
                                        <path refid="maven.plugin.classpath" />
                                    </classpath>
                                </taskdef>
                                <instrument verbose="true">
                                    <fileset dir="${project.build.outputDirectory}">
                                        <include name="com/example/domain/*.class" />
                                    </fileset>
                                </instrument>
                            </tasks>
                        </configuration>
                        <phase>process-classes</phase>
                        <goals>
                            <goal>run</goal>
                        </goals>
                    </execution>
                </executions>
                <dependencies>
                    <dependency>
                        <groupId>org.hibernate</groupId>
                        <artifactId>hibernate-core</artifactId>
                        <version>${hibernate.version}</version>
                    </dependency>
                    <dependency><!-- bytecode instrumentation library to use... note that cglib is deprecated -->
                        <groupId>org.javassist</groupId>
                        <artifactId>javassist</artifactId>
                        <version>${javassist.version}</version>
                    </dependency>
                    <dependency><!-- so that slf4j won't barf about missing binding -->
                        <groupId>org.slf4j</groupId>
                        <artifactId>slf4j-simple</artifactId>
                        <version>${slf4j.version}   </version>
                    </dependency>
                </dependencies>
            </plugin>
        </plugins>
    </build>
Note that you'll have to define the versions of libraries that you're using (here are my values):
  <properties>
    <hibernate.version>3.6.7.Final</hibernate.version>
    <javassist.version>3.14.0-GA</javassist.version>
    <slf4j.version>1.6.1</slf4j.version>
  </properties>

Wednesday, 14 March 2012

Deploying uPortal 4.0.3

I won't go here into details about configuring, compiling and other basic stuff. Main reason of this post is to give you some of my experience that you could use when deploying your own portal. While working on improvements, a lot of them have been given back to community as patches and will be included in upcoming releases of uPortal. Also best regards to Eric Dalquist (lead developer of uPortal) who helped me a lot with some performance (... and not only performance) issues. That allowed ORTUS (portal of Riga Technical University) to be one of the first (if not the first) production deployments of uPortal 4.0.x.

Required DB Schema changes for MS SQL Server

Since we are using MS SQL Server we had to disable or delete the single uniqueness index on table UP_DLM_EVALUATOR. This index should ensure that if fragment name is provided (is not null) then it must be unique. Unfortunately, MS SQL server is unable to provide such indexes since uniqueness check of 'null =? null' returns true when building index keys. This is not the case for other DBMS vendors (i.e. postgres).

Another pitfall was that we had to remove all @Transactional(readonly = true) annotations from DAO class methods. This annotation is required because selecting from text fields for Postgres requires read-only transaction. But this is a major slow-down for MS SQL Server.

Update (16.03.2012): If you're using SQL 2008 or later, you can (and should) create new filtered index to replace deleted index in order to maintain data integrity. Here's the required SQL query:

CREATE UNIQUE NONCLUSTERED INDEX [UP_DLM_FRAGMENT_NAME_UIDX] ON [dbo].[UP_DLM_EVALUATOR] ([FRAGMENT_NAME] ASC)
WHERE ([fragment_name] IS NOT NULL)

Memory configuration

uPortal 4.x uses caching a lot. This requires a decent amount of memory allocated for old generation objects. I would recommend you to dedicate around 4Gb of memory for old generation. There are guidelines stating that you should dedicate around half of memory for young generation (which did work well for us with uPortal 3.1.x) but for 4.x this won't work. Your server should have at least 8Gb RAM which should be partitioned as follows:

  • 6Gb for JVM (4Gb old and 2Gb new generations)
  • 512Mb for permanent generation - this will guarantee that in case of hot portlet deployment you won't run out of permanent generation space (note that you should also enable class unloading: -XX:+CMSClassUnloadingEnabled)
  • Leave the rest of memory for OS, other services (like nginx... which we're using in front of Tomcat) and OS caching.

With configuration above we get about 1s pause time each 8h on our server, which is not too bad but leaves a place for improvements. Full GCs can stop JVM for quite a while (especially in case of very big old generation space), hence I'm also thinking about switching to G1 garbage collector since it should lower pause times... but haven't field-tested it yet.

Cache configuration (ehcache.xml)

Another thing you should be aware about is caching. Default caches are nice, but in production environment you have to adjust your caches depending on your situation. What I've found out is that:

  • org.jasig.portal.groups.IEntity - default value TTL value is 6h. This means that after logging in your user won't get group updates next 6 hours. I found this troublesome and reduced it down to one hour and also set the idle time to half an hour.
  • org.jasig.portal.rendering.STRUCTURE_TRANSFORM - this cache rarely gets any hits (hit rate is around 0.1%), but consumes a lot of memory, hence reduced max element count in cache to 500 and set idle time to 10 minutes (600 seconds). I was also considering removing this cache at all, but since it gets some hits, then it does not hurt to leave it there.
  • org.jasig.portal.rendering.THEME_TRANSFORM - This cache is used a lot, but also uses quite a bit of memory, hence I've introduced max idle time with value of 10 minutes. With such configuration cache is usually filled with ~2k elements and hit rate is ~30-40% (for ~500 concurrent users... for more intensive use ).

Note that JConsole is your friend here. Make sure that you can connect with JConsole to your production machine since then you'll be able to adjust cache configuration of each cache instance at runtime. This is very useful.

Indexes for performance (fighting for milliseconds)

Here's a list of indexes that we're using. A lot of them also include columns so that after index seek, DB does not have to look up the value from table - column values are already returned by index. Since portal mostly issues SELECT statements, then we're using a lot of them.

CREATE INDEX UP_DLM_EVALUATOR_EVALUATOR_TYPE_FRAGMENT_NAME_IDX
  ON dbo.UP_DLM_EVALUATOR (EVALUATOR_TYPE, FRAGMENT_NAME)
CREATE INDEX UQ__UP_DLM_E__C0C7F43617C286CF
  ON dbo.UP_DLM_EVALUATOR (FRAGMENT_NAME)
CREATE INDEX UP_LOGIN_EVENT_AGGREGATE_INTERVAL_DATE_TIME_DIM_IDX
  ON dbo.UP_LOGIN_EVENT_AGGREGATE ([INTERVAL], DATE_DIMENSION_ID, TIME_DIMENSION_ID)
  INCLUDE (DURATION, LOGIN_COUNT, UNIQUE_LOGIN_COUNT, AGGREGATED_GROUP_ID)
CREATE INDEX UP_LOGIN_EVENT_AGGREGATE__UIDS_LOGIN_AGGR_ID_IDX
  ON dbo.UP_LOGIN_EVENT_AGGREGATE__UIDS (LOGIN_AGGR_ID)
  INCLUDE (uniqueUserNames)
CREATE INDEX UP_PRESON_ATTR_USER_DIR_ID_IDX
  ON dbo.UP_PERSON_ATTR (USER_DIR_ID)
  INCLUDE (id, ENTITY_VERSION, ATTR_NAME)
CREATE INDEX UP_PERSON_ATTR_VALUES_ATTR_ID_IDX
  ON dbo.UP_PERSON_ATTR_VALUES (ATTR_ID)
  INCLUDE (ATTR_VALUE, VALUE_ORDER)
CREATE UNIQUE INDEX UP_PERSON_DIR_USER_NAME_IDX
  ON dbo.UP_PERSON_DIR (USER_NAME)
  INCLUDE (USER_DIR_ID, ENTITY_VERSION, LST_PSWD_CGH_DT, ENCRPTD_PSWD)
CREATE INDEX UP_PORTAL_COOKIE_EXPIRES_IDX
  ON dbo.UP_PORTAL_COOKIES (EXPIRES)
CREATE UNIQUE INDEX UP_PORTALL_COOKIES_VALUE_IDX
  ON dbo.UP_PORTAL_COOKIES (COOKIE_VALUE)
  INCLUDE (PORTAL_COOKIE_ID, CREATED, ENTITY_VERSION, EXPIRES)
CREATE INDEX UP_PORTLET_COOKIES_PORTAL_COOKIE_ID_IDX
  ON dbo.UP_PORTLET_COOKIES (PORTAL_COOKIE_ID)
  INCLUDE (PORTLET_COOKIE_ID, COOKIE_COMMENT, COOKIE_DOMAIN, ENTITY_VERSION, EXPIRES, COOKIE_NAME, COOKIE_PATH, SECURE, COOKIE_VALUE, VERSION, portalCookie_PORTAL_COOKIE_ID)
CREATE INDEX UP_PORTLET_DEF_PARAM_PORTLET_DEF_ID_IDX
  ON dbo.UP_PORTLET_DEF_PARAM (PORLTET_DEF_ID)
CREATE INDEX UP_PORTLET_ENT_USER_ID_IDX
  ON dbo.UP_PORTLET_ENT (USER_ID)
CREATE INDEX UP_PORTLET_PREF_PORTLET_PREF_ID_IDX
  ON dbo.UP_PORTLET_PREF (PORTLET_PREF_ID)
CREATE INDEX UP_SS_USER_PREF_USER_ID_PROFILE_ID_IDX
  ON dbo.UP_SS_USER_PREF (PROFILE_ID, USER_ID)
  INCLUDE (SS_USER_PREF_ID, ENTITY_VERSION, UP_SS_DESCRIPTOR_ID)
CREATE INDEX UP_SS_USER_PREF_LAY_ATTR_SS_USER_PREF_ID_IDX
  ON dbo.UP_SS_USER_PREF_LAY_ATTR (SS_USER_PREF_ID)
  INCLUDE (UP_SS_USER_PREF_LAY_ATTR_ID, ENTITY_VERSION, NODE_ID)

Replace unique index on table UP_SS_USER_PREF with this one in order to include columns into index:

CREATE UNIQUE INDEX UP_SS_USER_PREF_PROFILE_USER_SSDESCRIPTOR_IDX
  ON dbo.UP_SS_USER_PREF (PROFILE_ID, USER_ID, UP_SS_DESCRIPTOR_ID)
  INCLUDE (SS_USER_PREF_ID, ENTITY_VERSION)

Tuesday, 31 January 2012

Customizing OpenAM pages

First, take a look at official WIKI page and documentation. Information there is pretty vague, but at least gives some hints. In the rest of this article I'll try to fill in some gaps.
I had to modify default login, logout and error pages for root realm. Although official documentation suggests not to modify default pages, but that was the solution that worked for me. I modified following pages in config/auth/default/ folder under OpenAM web context:
  • login_failed_template.jsp - displayed after unsuccessful login - usually because of invalid username/password
  • Login.jsp - login form
  • Logout.jsp - showed after user hits "logout" and terminates his SSO session
  • session_timeout.jsp - showed if user's session timed out due to inacitivty
The trick is to leave ALL jato:* elements in place. You can delete all tables, rows, replace them with DIVs, add additional DIV elements, etc, but JATO's must be left intact. I couldn't find a good documentation about that framework, hence all you can do is follow your intuition and find out the correct page structure using good old trial and error method.
In order to add additional localization (e.g. 'lv'):
  • Create new directory:
    mkdir /config/auth/default_lv
  • Copy the file that JATO uses for form i18n:
    cp /config/auth/default_en/DataStore.xml /config/auth/default_en/
  • Copy the file that contains all messages used int authentication views:
    cp WEB-INF/classes/amAuthUI.properties WEB-INF/classes/amAuthUI_lv.properties
  • ... translate copied files
None of *.jsp file changes require server restart since servlet container can pick up changes dynamically (at least Tomcat can), but .properties file changes won't be reflected until web context or server is restarted.
One more thing - you can change selected locale. If you want to provide users with an ability to switch between locales (override locale detected by browser request), you must specify a 'locale' request parameter (this one I had to dig out from source code). And in order to pass it to latter pages (not to lose it after unsuccessful login attempts), in the form at the end of Login.jsp add:
<input type="hidden" name="locale" value="<%=request.getParameter("locale")%>" />