Custom Velocity Tools and Liferay 6.0

Staff Blogs October 4, 2010 By Ray Augé Staff

A while back I wrote a post about adding custom tools to the Liferay Velocity context.

In 6.0 a change was made such that the behaviour has changed slightly. Now all such tools are plain old beans which must implement an interface.

The changes also means that I have a lot less code to write and less wiring to do. Let's see how we'd do it now using exactly the same tool as that old post.

The interface again:

package com.mytool;

public interface MyTool {

	public String operationOne();

	public String operationTwo(String name);

}

We need an implementation of the interface:

package com.mytool;

public class MyToolImpl implements MyTool {

	public String operationOne() {
		return "Hello out there!";
	}

	public String operationTwo(String name) {
		return "Hello " + name + "!";
	}

}

Our spring configuration only requires a single bean definition:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">

<beans>
	<bean id="com.mytool.MyTool" class="com.mytool.MyToolImpl" />
</beans>

Of course in order for our bean definitions to be read we need to make sure it gets loaded, so we'll name it so that it's found by the context loader WEB-INF/applicationContext.xml (We could have used the portal-esk spring config mechanism, but I wanted to demonstrate that Liferay is flexible and sensitive to existing coding behaviors.

I mentioned that we need a context loader, so there is one more change required. While before you could only add tools within a ServiceBuilder enabled plugin, that is no longer required. All you have to do is add the following context loader listener the your web.xml (but only if your plugin is not a portlet type plugin):

    <listener>
        <listener-class>com.liferay.portal.kernel.spring.context.PortletContextLoaderListener</listener-class>
    </listener>

This effectively tells the portal to create a BeanLocator object associated with your plugin and to read the beans definitions that were defined in WEB-INF/applicationContext.xml.

Now we're ready to use our tool in a Velocity template:

Since we've done this in a plugin, you will have to specify the 'contextPathName' of the plugin so that the appropriate BeanLocator can be used to lookup your tool. For example, the context path name of your plugin being "test-velotool-hook", then you'd use the following in your template:

#set ($myTool = $utilLocator.findUtil('test-velotool-hook', 'com.mytool.MyTool'))

$myTool.operationOne()

$myTool.operationTwo('Ray')

I've linked a source hook plugin that you can drop into your plugins SDK and deploy just by doing ant deploy.

[test-velotool-hook.zip]

Debugging Liferay in Eclipse

Staff Blogs September 22, 2010 By Ray Augé Staff

When should you use a debugger during development? At ALL times! At least that's my view.

Isn't that a hassle? Well debugging sure can be, but if you setup your environment in a specific way, it can actually become very fast and almost transaprent. I like transparent!

Here are steps I take to configure my tomcat (I've followed the same steps with JBoss):

  1. Note the deployment path of tomcat (in my case I'll use /bundles/tomcat-6.0.x)
  2. In Eclipse, open the "Run Configurations" manager (Run -> Run Configurations...)
  3. On the "Java Application" node, right click and choose "New".
  4. On the first tab (Main) select the project you are running in tomcat
  5. In the Main Class field enter org.apache.catalina.startup.Bootstrap
  6. On the Arguments tab, in the Program Arguments field enter start
  7. In the VM arguments field enter the following:

    -Xms1024m
    -Xmx1024m
    -XX:PermSize=128m
    -XX:MaxPermSize=256m
    -XX:+CMSClassUnloadingEnabled
    -Dorg.apache.catalina.loader.WebappClassLoader.ENABLE_CLEAR_REFERENCES=true
    -Dcatalina.base=/bundles/tomcat-6.0.x
    -Dcatalina.home=/bundles/tomcat-6.0.x
    -Djava.io.tmpdir=/bundles/tomcat-6.0.x/temp
    -Dexternal-properties=${workspace_loc:<NAME_OF_YOUR_ECLIPSE_PROJECT>}/portal-web/docroot/WEB-INF/src/portal-developer-tomcat.properties
    -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager
    -Djava.util.logging.config.file=/bundles/tomcat-6.0.x/conf/logging.properties


    You many notice that I included a reference to my portal project's portal-developer-tomcat.properties file. This way I can change portal settings and only need to restart the portal without a redeploy, which is very convenient.

    You may also notice that I included tomcat's juli log manager so that all the normal web app configurations work as expected and in the Eclipse console, which is also very nice.

    Adjust your heap settings as desired, but the above settings have worked for me in 99.999% of cases.
     
  8. In the Working directory field choose "Other" and enter /bundles/tomcat-6.0.x/bin. Notice that this is the tomcat path, plus the /bin folder.
  9. The JRE tab should be already set properly, but you can choose any JRE that is installed that is 1.5+.
  10. On the Classpath tab, remove any entries under User Entries and then choose "Add External JARs...".
  11. Select the 3 jars located in /bundles/tomcat-6.0.x/bin.
  12. On the Source tab make sure that you have both Default, as well as your project in the list.

    Optionally here you can add any plugin projects that you are working on, as well as adding the path to the jsp java classes (typically /bundles/tomcat-6.0.x/work/Catalina/localhost/_)
  13. On the Common tag choose at least Debug and Run under "Display in favorites menu".
  14. Click "Apply" near the bottom of the dialog.
  15. Finally, to start the portal in debug mode, click the Debug menu in the Eclipse toolbar, and choose the run config you jsut created.

    You should see at this point output on the Eclipse console showing that tomcat is starting up.

I've been using this techinque for at least 3 years and I run the portal this way 99.999% of the time. By doing so, I can at any point in time add a breakpoint and start trouble shooting and I don't have to worry about re-connecting to or restarting the portal in debug mode, I know it's already done.

If you want to add any other java apps this way, the simples technique is to first run the app in the traditional way, then observe the process log to see what the full command that was actually used to start it, and from where. Once you know that, you can add any app to Eclipse Run Configuration.
 

 

Listing Article fragments dynamically using Liferay's WCM Templates

Staff Blogs September 16, 2010 By Ray Augé Staff

 So my last blog post involved a classloader trick for velocity but it also involved a problem of dynamically listing article fragments on a page.

Now, there are a few ways to do this, but the two that come to mind are:

1) Render each article using a specified template which only shows the content of the desired fields (fine, but seems overkill)

2) Parse the article content XML to get only specified bits (also fine, but there are concerns when the content is localized, not to mention isn't the XML handling code ugly/heavy?)

I've mostly taken approach 1), but could approach 2) be clean and light if we do it right? I think it can!

Here is how I did it (using the trick from the last post of course ):

#set ($journalArticleLocalService = $serviceLocator.findService('com.liferay.portlet.journal.service.JournalArticleLocalService'))
#set ($localeTransformer = $portal.getClass().forName('com.liferay.portlet.journal.util.LocaleTransformerListener').newInstance())
#set ($VOID = $localeTransformer.setLanguageId($request.theme-display.language-id))

#set ($companyId = $getterUtil.getLong($request.theme-display.company-id))
#set ($scopeGroupId = $getterUtil.getLong($request.theme-display.scope-group-id))

#set ($obc = $portal.getClass().forName("com.liferay.portlet.journal.util.comparator.ArticleDisplayDateComparator").newInstance())

#set ($articles = $journalArticleLocalService.search($companyId, $scopeGroupId, '', null, null, null, null, null, null, 0, null, -1, -1, $obc))

<ul> 
#foreach ($article in $articles)
     #set ($xml = $localeTransformer.onXml($article.content))
     #set ($doc = $saxReaderUtil.read($xml))
     #set ($title = $doc.valueOf("//dynamic-element[@name='title']/dynamic-content/text()"))
     <li>${title}</li>
#end
</ul>

 

Now that's contrived, but it's pretty lean, mean and straight to the point, which is always good.

Tricks of Velocity class loading

Staff Blogs September 16, 2010 By Ray Augé Staff

Recently I was asked to solve a WCM dynamic Template problem which involved listing articles using the business logic of the template.

One of the first problems I encountered was of course doing a search for the Articles via the JournalArticleLocalService which, in the case of the searchs which return List<JournalArticle>, require an OrderByComparator. Since there is no way to directly instantiate classes in Velocity I wondered how I was going to handle this issue.

Luckily my colleague Thiago was working with a community member (Sabrina Schürhaus Locks) who had already solved the problem in a rather interesting and simple way:

 

#set ($obc = $portal.getClass().forName("com.liferay.portlet.journal.util.comparator.ArticleDisplayDateComparator").newInstance())

 

It's so simple I feel silly that I never thought of this myself.

Kudos Sabrina!

Writing Liferay Plugins with Groovy

Staff Blogs July 24, 2010 By Ray Augé Staff

We've been saying for a long time that you could write plugins in a variety of languages, but we never really had any examples to prove that.

Of course some languages will be far simpler to achieve this that others. It'll be far simpler to do this using languages that have native java bindings. Groovy is writen natively in java and even extends the JDK with awesome new features, so that's the one I'm choosing.

Another concern is speed of delievery while not comprimising on performance, stability, ability to debug, etc.

Groovy has awesome tooling support under Eclipse, and even takes part in debugging with little problem at all. All I had to do was install the Groovy Eclipse plugin and add the Groovy nature to my plugin project (yes, I'm using the LIDE. But since this isn't a blog about LIDE, I'm not gonna waste time showing that. It's safe to say that it wasn't a huge effort to add the Groovy support to the .project file and the IDE came in handy when specifying the props configurations which were few).

So, I'm going to demonstrate how to write a hook in Groovy. I want to demonstate 2 things in particular:

  1. That you can indeed write Liferay plugins using another language, in this case Groovy.
  2. That you can use the power of a wonderful language like Groovy to achieve things that would take an order of magnitude more code if you were to try with java.

I also have three goals:

  1. I'm going to implement a single listener of as many model events as I choose, listenting to as many model types as I choose, without having to write more that a single implementation.
  2. I'm going to persist audit events to the DB using as little persistence code as physically possible.
  3. I'm going to do it really fast (I'm pretending my boss wanted this done last week...).

So, where do I start?

The first thing I need to do is support compiling my groovy code. To do that I'm going to override the default ant target which normally compiles the plugin code, but only for this one plugin (Cause I don't want to harm other projects until I have this nailed down to a science.)

  • First thing to do is download the latest Groovy jar and add that to your plugin's <project>/docroot/WEB-INF/lib folder (I used groovy-all-1.7.3.jar, which was the latest version at the time of writting).
     
  • Next, I'll open up <project>/build.xml and paste the following ant target definition:
                <target name="compile">
                    <antcall target="merge" />
                
                    <mkdir dir="docroot/WEB-INF/classes" />
                    <mkdir dir="docroot/WEB-INF/lib" />
                
                    <copy todir="docroot/WEB-INF/lib">
                        <fileset dir="${app.server.lib.portal.dir}" includes="${plugin.jars}" />
                    </copy>
                
                    <copy todir="docroot/WEB-INF/tld">
                        <fileset dir="${app.server.portal.dir}/WEB-INF/tld" includes="${plugin.tlds}" />
                    </copy>
                
                    <if>
                        <available file="docroot/WEB-INF/src" />
                        <then>
                            <if>
                                <available file="tmp" />
                                <then>
                                    <path id="plugin-lib.classpath">
                                        <fileset dir="docroot/WEB-INF/lib" includes="*.jar" />
                                        <fileset dir="tmp/WEB-INF/lib" includes="*.jar" />
                                        <pathelement location="docroot/WEB-INF/classes" />
                                        <pathelement location="tmp/WEB-INF/classes" />
                                    </path>
                                </then>
                                <else>
                                    <path id="plugin-lib.classpath">
                                        <fileset dir="docroot/WEB-INF/lib" includes="*.jar" />
                                        <pathelement location="docroot/WEB-INF/classes" />
                                    </path>
                                </else>
                            </if>
                
                            <copy todir="docroot/WEB-INF/lib">
                                <fileset dir="${app.server.lib.portal.dir}" includes="${required.portal.jars}" />
                            </copy>
                
                            <if>
                                <available file="docroot/WEB-INF/lib/portal-impl.jar" />
                                <then>
                                    <fail>
                .
                
                Detected inclusion of portal-impl.jar in WEB-INF/lib.
                
                portal-impl.jar is designed with a large number of singleton classes which are
                instantiated on the basis that they will exist alone in the application server.
                
                While compile time issues may be resolved, portlets cannot be made to work by
                simply adding portal-impl.jar, because doing so violates the above assumption,
                and the resulting problems will be extremely difficult to debug.
                
                Please find a solution that does not require portal-impl.jar.
                                        </fail>
                                </then>
                            </if>
                
                            <taskdef name="groovyc"
                    classname="org.codehaus.groovy.ant.Groovyc"
                    classpathref="plugin.classpath"/>

                <groovyc
                    classpathref="plugin.classpath"
                    destdir="docroot/WEB-INF/classes"
                    srcdir="docroot/WEB-INF/src"
                >
                    <javac
                        compiler="${javac.compiler}"
                        debug="${javac.debug}"
                        deprecation="${javac.deprecation}"
                        fork="${javac.fork}"
                        memoryMaximumSize="${javac.memoryMaximumSize}"
                        nowarn="${javac.nowarn}"
                    />
                </groovyc>
            </then>     </if>     <antcall target="merge" /> </target>

     
    Note the part in red that replaces the default compiler call with one to groovyc (Don't worry, this will also compile any java code seamlessly if there is some in the project).
     

  • Now I make sure that my hook has a properties file defined cause I'm going to write an application Startup event, and a model listener:
    <hook>
        <portal-properties>portal.properties</portal-properties>
    </hook>
     
  • My two implementations are going to be called com.liferay.sample.groovy.GStartupAction and com.liferay.sample.groovy.GModelListener so I'll set those up in the props file (for now I'm just picking a whole bunch of interesting models to listen to, I can tune this again later):
    application.startup.events=com.liferay.sample.groovy.GStartupAction

    value.object.listener.com.liferay.portal.model.User=com.liferay.sample.groovy.GModelListener
    value.object.listener.com.liferay.portal.model.Layout=com.liferay.sample.groovy.GModelListener
    value.object.listener.com.liferay.portlet.blogs.model.BlogsEntry=com.liferay.sample.groovy.GModelListener
    value.object.listener.com.liferay.portlet.blogs.model.BlogsStatsUser=com.liferay.sample.groovy.GModelListener
    value.object.listener.com.liferay.portlet.journal.model.JournalArticle=com.liferay.sample.groovy.GModelListener
    value.object.listener.com.liferay.portlet.journal.model.JournalStructure=com.liferay.sample.groovy.GModelListener
    value.object.listener.com.liferay.portlet.journal.model.JournalTemplate=com.liferay.sample.groovy.GModelListener
    value.object.listener.com.liferay.portlet.messageboards.model.MBCategory=com.liferay.sample.groovy.GModelListener
    value.object.listener.com.liferay.portlet.messageboards.model.MBMessage=com.liferay.sample.groovy.GModelListener
    value.object.listener.com.liferay.portlet.messageboards.model.MBThread=com.liferay.sample.groovy.GModelListener
     
  • Ok, so now we have all the house keeping stuff done, and I can get to work on writting the code. For the sake of speed, I'm using the simplest type of DB interaction that comes in the form of Groovy's groovy.sql.Sql class which easily lets me do JDBC operations using very lean amount of code (my DB operations are on MySQL specifically, again for the sake of speed, I'm not concerned with syntax that will work with other DBs).

    The first thing I need to do is create the DB table, if it doesn't exist. I'm going to do that in my GStartupAction:
    package com.liferay.sample.groovy

    import com.liferay.portal.kernel.events.SimpleAction
    import com.liferay.portal.kernel.util.InfrastructureUtil

    import groovy.sql.Sql

    class GStartupAction extends SimpleAction {

        public void run(String[] arg0) {
            try {
                _sql.rows('select count(*) from AuditLog')
            }
            catch (e) {
                _sql.execute '''
                    create table AuditLog (
                        auditId bigint not null primary key,
                        groupId bigint,
                        className varchar(75),
                        classPK bigint,
                        classUuid varchar(75),
                        auditDate datetime,
                        description varchar(200),
                        model longtext
                    )
                '''
            }
        }

        private static final Sql _sql = new Sql(InfrastructureUtil.dataSource)

    }
     You gotta admit that's pretty short. That's it for that class.
     
  •  On to GModelListener. Note that we want it to work with any of the models we throw at it. And we also want to limit the code, so we're going to extend com.liferay.portal.model.BaseModelListener<T>. The only events we care about are: onBeforeCreate, onBeforeRemove, onBeforeUpdate.
    package com.liferay.sample.groovy

    import com.liferay.portal.model.BaseModelListener

    class GModelListener extends BaseModelListener {

        void onBeforeCreate(model) {
        }

        void onBeforeRemove(model) {
        }

        void onBeforeUpdate(model) {
        }

    }
    We start with the above.
     
  • I need a couple other objects setup, logging, and the Groovy Sql object that I need to do the DB operations:
    package com.liferay.sample.groovy

    import com.liferay.portal.kernel.log.LogFactoryUtil
    import com.liferay.portal.kernel.util.InfrastructureUtil;
    import com.liferay.portal.model.BaseModelListener

    class GModelListener extends BaseModelListener {

        void onBeforeCreate(model) {
        }

        void onBeforeRemove(model) {
        }

        void onBeforeUpdate(model) {
        }

        private static final _log = LogFactoryUtil.getLog(GModelListener.class)

        private static final _sql = new Sql(InfrastructureUtil.dataSource)

        private static final _auditLog = _sql.dataSet("AuditLog")

    }

    The _auditLog variable is a Groovy groovy.sql.DataSet object that lets me do really clean operations on a given table.
     
  • Finally, I want to write a closure that will do the work of updating the AuditLog table, but first I want a very simple API for it:
    package com.liferay.sample.groovy

    import com.liferay.portal.kernel.log.LogFactoryUtil
    import com.liferay.portal.kernel.util.InfrastructureUtil;
    import com.liferay.portal.model.BaseModelListener

    class GModelListener extends BaseModelListener {

        void onBeforeCreate(model) {
            audit(model, "onBeforeCreate")
        }

        void onBeforeRemove(model) {
            audit(model, "onBeforeRemove")
        }

        void onBeforeUpdate(model) {
            audit(model, "onBeforeUpdate")
        }

        def audit = { model, message ->
            // do my work here
        }

        private static final _log = LogFactoryUtil.getLog(GModelListener.class)

        private static final _sql = new Sql(InfrastructureUtil.dataSource)

        private static final _auditLog = _sql.dataSet("AuditLog")

    }

    That's pretty straight forward! It'll do the trick.
     
  • Now, I want to track who did the particular operation, so I need to current user's Id, so it can be recoreded. The simplest way to do this is to get the current PermissionChecker and if it exists, then record the userId:

        def audit = { model, message ->
            def userId = PermissionThreadLocal.permissionChecker?.userId
        }

     
  • We also want the Group, anbd for the sake of completeness, if the model has a uuid field we want that too:

        def audit = { model, message ->

            def userId = PermissionThreadLocal.permissionChecker?.userId

            def groupId = 0
            def uuid = ''

        }


    But, how do we handle with the models without those fields? Isn't there a bunch of reflection involed in handling things like that? Well, in java, Yes! In Groovy, No! This is so easy in Groovy it's almost trivial:

        def audit = { model, message ->

            def userId = PermissionThreadLocal.permissionChecker?.userId

            def groupId = 0
            def uuid = ''

            if (model.metaClass.respondsTo(model, 'getGroupId')) {
                groupId = model.groupId
            }

            if (model.metaClass.respondsTo(model, 'getUuid')) {
                uuid = model.uuid
            }
        }


    That's to say, if the model object has a given method, we'll just call it. If not, well we just ignore it and keep the default value.
  • Next we'll populate a map of data we want to store in the AuditLog table:

        def audit = { model, message ->

            def userId = PermissionThreadLocal.permissionChecker?.userId

            def groupId = 0
            def uuid = ''

            if (model.metaClass.respondsTo(model, 'getGroupId')) {
                groupId = model.groupId
            }

            if (model.metaClass.respondsTo(model, 'getUuid')) {
                uuid = model.uuid
            }

            def map = [
                auditId: CounterLocalServiceUtil.increment(),
                groupId: groupId,
                className: model.class.name,
                classPK: model.primaryKey,
                classUuid: uuid,
                auditDate: new Date(),
                description: message + " by " + String.valueOf(userId),
                model: model.toString()
            ]

        }


    Simple enough!
     
  • Now, let's store and log the result:

        def audit = { model, message ->

            def userId = PermissionThreadLocal.permissionChecker?.userId

            def groupId = 0
            def uuid = ''

            if (model.metaClass.respondsTo(model, 'getGroupId')) {
                groupId = model.groupId
            }

            if (model.metaClass.respondsTo(model, 'getUuid')) {
                uuid = model.uuid
            }

            def map = [
                auditId: CounterLocalServiceUtil.increment(),
                groupId: groupId,
                className: model.class.name,
                classPK: model.primaryKey,
                classUuid: uuid,
                auditDate: new Date(),
                description: message + " by " + String.valueOf(userId),
                model: model.toString()
            ]

            _auditLog.add(
                auditId: map.auditId, groupId: map.groupId,
                className: map.className, classPK: map.classPK,
                classUuid: map.classUuid, auditDate: map.auditDate,
                description: map.description, model: map.model)

            if (_log.infoEnabled) {
                _log.info map
            }
        }


    Phew!!! We're done. We got it all done before lunch time.

    Arguably, we could have saved another 13 lines od code if we didn't need the map that we used for both persisting and logging the event. We could just as easily passed the raw values to the _auditLog instance, but wait.. we don't want the code to be TOO short, otherwise our "lines of code contributed" factor will make it look like we never do ANY work at all!!!
     
  • The whole class looks like this:
    package com.liferay.sample.groovy

    import com.liferay.counter.service.CounterLocalServiceUtil
    import com.liferay.portal.kernel.log.LogFactoryUtil
    import com.liferay.portal.kernel.util.InfrastructureUtil;
    import com.liferay.portal.model.BaseModelListener
    import com.liferay.portal.security.permission.PermissionThreadLocal;

    import groovy.sql.Sql

    class GModelListener extends BaseModelListener {

        void onBeforeCreate(model) {
            audit(model, "onBeforeCreate")
        }

        void onBeforeRemove(model) {
            audit(model, "onBeforeRemove")
        }

        void onBeforeUpdate(model) {
            audit(model, "onBeforeUpdate")
        }

        def audit = { model, message ->
            def userId = PermissionThreadLocal.permissionChecker?.userId

            def groupId = 0
            def uuid = ''

            if (model.metaClass.respondsTo(model, 'getGroupId')) {
                groupId = model.groupId
            }

            if (model.metaClass.respondsTo(model, 'getUuid')) {
                uuid = model.uuid
            }

            def map = [
                auditId: CounterLocalServiceUtil.increment(),
                groupId: groupId,
                className: model.class.name,
                classPK: model.primaryKey,
                classUuid: uuid,
                auditDate: new Date(),
                description: message + " by " + String.valueOf(userId),
                model: model.toString()
            ]

            _auditLog.add(
                auditId: map.auditId, groupId: map.groupId,
                className: map.className, classPK: map.classPK,
                classUuid: map.classUuid, auditDate: map.auditDate,
                description: map.description, model: map.model)

            if (_log.infoEnabled) {
                _log.info map
            }
        }

        private static final _log = LogFactoryUtil.getLog(GModelListener.class)

        private static final _sql = new Sql(InfrastructureUtil.dataSource)

        private static final _auditLog = _sql.dataSet("AuditLog")

    }

    Now you can go back and add the missing models and you're good to go.

    Oh, and when you're done, take the rest of the day off... you deserve it!

Liferay Staging 6.0

Staff Blogs July 15, 2010 By Ray Augé Staff

Some of the latest details about Liferay Staging in 6.0 are now on the Wiki: Staging - 6.0

  • Local Live
  • Remote Live
  • Better Differential Publishing
  • Stored connections settings for remote staging (as you'd expect)
  • Un-Staged Portlets (pick and choose which portlets are staged)
  • Lots and Lots of fixes and usability improvements

Let us know what you think about it.

Ideas for Blog and WCS talks

Staff Blogs June 22, 2010 By Ray Augé Staff

I haven't been blogging as much as I'd like...

My return to core after a long year+ on a project hasn't turned out quite the way I had envisioned.  (Don't get me wrong, I'm SOOOO happy to be back; although, to be fair I was with an awesome client.)

I had two particular things in mind when coming back:

1) lots of new code to play with
2) lots of blogging about all things Liferay

Part 1) "lots of new code to play with" has turned out as I expected, and more. So much code....   The sad thing is that it also has gotten in the way of 2) "lots of blogging about all things Liferay".

 

 

Really, there are so many new features and refinements in Liferay 6.0 that it's mind boggling to think of how much innovation has taken place, and I missed so much of it. I frankly sometimes don't know where to start.

In fact, I was so mind boggled the weeks after being back that during lead up to ECS I got so confused over what and who was doing what that I ended up not having a single talk about code. And to be honest, I should never speak publicly about anything except code. Thankfully Brett saved me and did a talk in my place. (Thanks Brett! I know it wasn't all a win, win for you.)

Anyhow, I promise that WCS will be a different story. I will speak about nothing but code. In fact, I will actually speak IN code in order to make up for the missed opportunity at ECS . (Just kidding!)

No but honestly, I want to know what would be the most compelling topic(s) for me to discuss at WCS. I have several ideas; some may turn up as blog posts, but some could be specially for WCS.

I'm still planning to write about groovy|ruby plugins in the near future. I could always talk about the latest way to RAD on Liferay (which is always of interest to me personally).

But hey, if you're planning to be at WCS, and you have a topic that you'd like me to talk about, throw it up here and I'll see if we can work it in.

Whatever the topic, I can try to speak a little on it whether I do a full blog on it or not (that is, unless I know who can give a better answer), so don't hold back (too much)!

 

Ideas?

Debuging Ant classpath issues

Staff Blogs May 29, 2010 By Ray Augé Staff

When working with Liferay, sometimes you might encounter classpath issues during ant builds.

I have a little trick which may help.

It's a small bit of xml that you can add to a target which dumps a classpath by refid:

 

 <property name="debug.classpath" refid="service.classpath"/>
            
 <for list="${debug.classpath}" delimiter=":" param="cur">
     <sequential>
         <echo>@{cur}</echo>
     </sequential>
 </for>

 

That's it! Short and sweet.

Setting tabwidths to match the project settings in GIT and VI

Staff Blogs May 19, 2010 By Ray Augé Staff

I use Git and VI(m) quite a bit, and in the Liferay project the source format rules define that leading whitespace character is tab (with potentially trainling spaces before the rest of the line in some cases, but that's another story).

Furthermore, tab width is 4.

Since I'm commonly in the command line I hate that the source doesn't represent the same way that I see it in the IDE, especially when reviewing diffs since most console output has tab width of 8.

GIT

So the question is "How do I make git output use tab width of 4 instead of the default 8?"

After searching long without success, I finnally learned that git uses the "pager" paradigm for displaying diffs, and that the external pager can be tuned in the ~/.gitconfig file.

Awesome!

So, to adjust the tab width add the following to your config:

[core]
    pager = less -FXRS -x4

 

This tells git to use "less" command as the external pager, then it passes some settings to less, but most importantly it sends the setting:

-x<n>

which sets the tab width.

VI(m)

Now to get the same result in VI(m) you can edit the ~/.vimrc file. Coolness!

set tabstop=4
set ts=4
set shiftwidth=4
set autoindent

 

Great! That's it.

Now all my important console output has the proper tab widths (and I can tell if I messed up my bchan style formatting).  

;)

Git the most out of your bash prompt

Staff Blogs May 14, 2010 By Ray Augé Staff

I was looking for a way to add as much relevant info about my Liferay git repository to my prompt as would not slow it down to much, cause I hated having to constantly do git branch/status to find branch/state all the time.

So after reading a couple nice posts about it,

http://henrik.nyh.se/2008/12/git-dirty-prompt

http://plasti.cx/2009/10/23/vebose-git-dirty-prompt

I took the best of both of those (performance wise and output wise), and since I use git-svn added to mine the requirement of showing the svn revision my branch is currently synced with.

Here is the bash code I added to by ~/.bashrc :

function parse_git_dirty {
status=`git status 2> /dev/null`
        dirty=`    echo -n "${status}" 2> /dev/null | grep -q "Changed but not updated" 2> /dev/null; echo "$?"`
        untracked=`echo -n "${status}" 2> /dev/null | grep -q "Untracked files" 2> /dev/null; echo "$?"`
        ahead=`    echo -n "${status}" 2> /dev/null | grep -q "Your branch is ahead of" 2> /dev/null; echo "$?"`
        newfile=`  echo -n "${status}" 2> /dev/null | grep -q "new file:" 2> /dev/null; echo "$?"`
        renamed=`  echo -n "${status}" 2> /dev/null | grep -q "renamed:" 2> /dev/null; echo "$?"`
        bits=''
        if [ "${dirty}" == "0" ]; then
                bits="${bits}☭"
        fi
        if [ "${untracked}" == "0" ]; then
                bits="${bits}?"
        fi
        if [ "${newfile}" == "0" ]; then
                bits="${bits}*"
        fi
        if [ "${ahead}" == "0" ]; then
                bits="${bits}+"
        fi
        if [ "${renamed}" == "0" ]; then
                bits="${bits}>"
        fi
        echo "${bits}"
}

function parse_git_svn_revision {
        ref1=$(__git_ps1 | sed -e "s/ (\(.*\))/(git: \1$(parse_git_dirty))/")
        #ref1=$(parse_git_branch)

        if [ "x$ref1" != "x"  ]; then
                ref2=$(git svn info | grep Revision)
                echo " ${ref1} (svn: r"${ref2#Revision: }") "
        fi
}

PS1='\[\033[0;37m\][\[\033[0;31m\]\u@\h\[\033[0;33m\]`parse_git_svn_revision`\[\033[0;32m\]\W\[\033[0;37m\]]\$ '

 
This is what it looks like:

the output of a bash session using the new prompt

 

As per Plasticx's blog the dirty flags are as such:

  • ‘☭’ – files have been modified
  • ‘?’ – there are untracted files in the project
  • ‘*’ – a new file has been add to the project but not committed
  • ‘+’ – the local project is ahead of the remote
  • ‘>’ – file has been moved or renamed

 

Much better!

Expandos II (refactor of a previous post for 6.0+)

Staff Blogs April 29, 2010 By Ray Augé Staff

Quite a while ago I wrote an article on Liferay's Expandos (on top of which our Custom Attributes (Custom Fields) are built), using the WCM as the runtime for a small app called First Expando Bank.

Since that time the API has undergone slight alterations and so since there was so much interest in it, here is new implementation which does a better job of showing how to use the API by:

  1. Making less service calls
  2. Demonstrating Pagination

Here is the updated template:

##
## Do some request handling setup.
##

#set ($companyId = $getterUtil.getLong($request.theme-display.company-id))
#set ($locale = $localeUtil.fromLanguageId($request.locale))
#set ($dateFormatDateTime = $dateFormats.getDateTime($locale))

#set ($renderUrl = $request.render-url)
#set ($pns = $request.portlet-namespace)
#set ($cmd = $getterUtil.getString($request.parameters.cmd))

#set ($cur = $getterUtil.getInteger($request.parameters.cur, 1))
#set ($delta = $getterUtil.getInteger($request.parameters.delta, 5))

#set ($end = $cur * $delta)
#set ($start = $end - $delta)

<h1>First Expando Bank</h1>

##
## Define the "name" for our ExpandoTable.
##

#set ($accountsTableName = "AccountsTable")

##
## Get/Create the ExpandoTable to hold our data.
##

#set ($accountsTable = $expandoTableLocalService.getTable($companyId, $accountsTableName, $accountsTableName))
#set ($accountsTableId = $accountsTable.getTableId())

#if (!$accountsTable)
	#set ($accountsTable = $expandoTableLocalService.addTable($companyId, $accountsTableName, $accountsTableName))
	#set ($accountsTableId = $accountsTable.getTableId())

	#set ($VOID = $expandoColumnLocalService.addColumn($accountsTableId, "firstName", 15)) ## STRING
	#set ($VOID = $expandoColumnLocalService.addColumn($accountsTableId, "lastName", 15)) ## STRING
	#set ($VOID = $expandoColumnLocalService.addColumn($accountsTableId, "balance", 5)) ## DOUBLE
	#set ($VOID = $expandoColumnLocalService.addColumn($accountsTableId, "modifiedDate", 3)) ## DATE
#end

#set ($accountsTableClassNameId = $accountsTable.getClassNameId())
#set ($columns = $expandoColumnLocalService.getColumns($accountsTableId))

##
## Check to see if a classPK was passed in the request.
##

#set ($classPK = $getterUtil.getLong($request.parameters.classPK))

##
## Check if we have received a form submission?
##

#if ($cmd.equals('add') || $cmd.equals('update'))
	##
	## Let's get the form values from the request.
	##

	#set ($firstName = $getterUtil.getString($request.parameters.firstName, ''))
	#set ($lastName = $getterUtil.getString($request.parameters.lastName, ''))
	#set ($balance = $getterUtil.getDouble($request.parameters.balance, 0.0))
	#set ($date = $dateTool.getDate())

	##
	## Validate the params to see if we should proceed.
	##

	#if ($balance < 50)
		Please fill the form completely in order to create an account. The minimum amount of cash required to create an account is $50.
	#elseif (!$firstName.equals('') && !$lastName.equals(''))
		##
		## Check to see if it's a new Account.
		##

		#if ($classPK <= 0)
			#set ($classPK = $dateTool.getDate().getTime())
		#end

		#set ($VOID = $expandoValueLocalService.addValues($accountsTableClassNameId, $accountsTableId, $columns, $classPK, {'firstName':$firstName, 'lastName':$lastName, 'balance':"$balance", 'modifiedDate':"${date.getTime()}"}))

		##
		## Show a response.
		##

		#if ($cmd.equals('update'))
			Thank you, ${firstName}, for updating your account with our bank!
		#else
			Thank you, ${firstName}, for creating an account with our bank!
		#end
	#else
		Please fill the form completely in order to create an account. Make sure to till both first and last name fields.
	#end
#elseif ($cmd.equals('delete'))
	##
	## Delete the specified Row.
	##

	#if ($classPK > 0)
		#set ($VOID = $expandoRowLocalService.deleteRow($accountsTableId, $classPK))

		Account deleted!

		#set ($classPK = 0)
	#end
#elseif ($cmd.equals('edit'))
	##
	## Edit the specified Row.
	##

	Editting...
#end

<span style="display: block; border-top: 1px solid #CCC; margin: 5px 0px 5px 0px;"></span>

#if (!$cmd.equals('edit'))
	##
	## Now we're into the display logic.
	##

	<input type="button" value="Create Account" onClick="self.location = '${renderUrl}&${pns}cmd=edit';" />

	<br /><br />

	<table class="taglib-search-iterator">
	<tr class="results-header">
		<th class="col-1">Account Number</th>
		<th class="col-2">First Name</th>
		<th class="col-3">Last Name</th>
		<th class="col-4">Balance</th>
		<th class="col-5">Modified Date</th>
		<th class="col-6"><!----></th>
	</tr>

	##
	## Get all the current records in our ExpandoTable. We can paginate by passing a
	## "begin" and "end" params.
	##

	#set ($total = $expandoRowLocalService.getRowsCount($accountsTableId))
	#set ($rows = $expandoRowLocalService.getRows($accountsTableId, $start, $end))

	#foreach($row in $rows)
		#set ($cssClass = 'results-row')

		#if ($velocityCount % 2 == 0)
			#set ($cssClass = "${cssClass} alt")
		#end

		#if ($velocityCount == 1)
			#set ($cssClass = "${cssClass} first")
		#elseif ($velocityCount == $rows.size())
			#set ($cssClass = "${cssClass} last")
		#end

		##
		## Get the classPK of this row.
		##

		#set ($currentClassPK = $row.getClassPK())

		#set ($rowValues = $expandoValueLocalService.getRowValues($row.getRowId()))

		#set ($values = {})

		#foreach ($value in $rowValues)
			#foreach ($column in $columns)
				#if ($value.columnId == $column.columnId)
					#set ($VOID = $values.put($column.name, $value))
				#end
			#end
		#end

		#set ($currentFirstName = $values.firstName.string)
		#set ($currentLastName = $values.lastName.string)
		#set ($currentBalance = $values.balance.double)
		#set ($currentModifiedDate = $values.modifiedDate.date)

		<tr class="${cssClass}">
			<td class="align-left col-1 valign-left">${currentClassPK}</td>

			<td class="align-left col-2 valign-middle">${currentFirstName}</td>

			<td class="align-left col-3 valign-middle">${currentLastName}</td>

			<td class="align-right col-4 valign-middle">${numberTool.currency($currentBalance)}</td>

			<td class="align-left col-5 valign-middle">${dateFormatDateTime.format($currentModifiedDate)}</td>

			<td class="align-right col-6 valign-middle">
				<a href="${renderUrl}&amp;${pns}cmd=edit&amp;${pns}classPK=${currentClassPK}">Edit</a> |
				<a href="${renderUrl}&amp;${pns}cmd=delete&amp;${pns}classPK=${currentClassPK}">Delete</a>
			</td>
		</tr>
	#end

	#if ($total <= 0)
		<tr>
			<td colspan="5">No Accounts were found.</td>
		</tr>
	#end

	</table>

	<br/>

	#if ($total > $delta)
		<div style="float: right;">
			<div>
				#set ($previous = $cur - 1)
				#set ($next = $cur + 1)

				#if ($previous > 0)
					<a href="${renderUrl}&${pns}cur=${previous}" class="previous">‹‹ #language('previous')</a>
				#else
					<span class="previous">‹‹ #language('previous')</span>
				#end

				#set ($pagesIteratorBegin = 1)
				#set ($pagesIteratorEnd = $total / $delta)
				#if (($total % $delta) > 0)
					#set ($pagesIteratorEnd = $pagesIteratorEnd + 1)
				#end

				#foreach ($index in [$pagesIteratorBegin..$pagesIteratorEnd])
					#if ($index == $cur)
						#set ($pageNumber = "<strong>${index}</strong>")
					#else
						#set ($pageNumber = $index)
					#end

					<a href="${renderUrl}&${pns}cur=${index}" class="previous">${pageNumber}</a>
				#end

				#if ($next > $cur && $next <= $pagesIteratorEnd)
					<a href="${renderUrl}&${pns}cur=${next}" class="next">#language('next') ››</a>
				#else
					<span class="next">#language('next') ››</span>
				#end
			</div>
		</div>
	#end

	# of Accounts: ${total}
#else
	##
	## Here we have our input form.
	##

	#if ($classPK > 0)
		##
		## Get the account specific values
		##

		#set ($rowValues = $expandoValueLocalService.getRowValues($companyId, $accountsTableName, $accountsTableName, $classPK, -1, -1))

		#set ($values = {})

		#foreach ($value in $rowValues)
			#foreach ($column in $columns)
				#if ($value.columnId == $column.columnId)
					#set ($VOID = $values.put($column.name, $value))
				#end
			#end
		#end

		#set ($currentFirstName = $values.firstName.string)
		#set ($currentLastName = $values.lastName.string)
		#set ($currentBalance = $values.balance.double)
	#end

	<form action="$renderUrl" method="post" name="${pns}fm10">
	<input type="hidden" name="${pns}classPK" value="$!{classPK}" />
	<input type="hidden" name="${pns}cmd" #if ($classPK > 0) value="update" #else value="add" #end/>

	<table class="lfr-table">
	<tr>
		<td>First Name:</td>
		<td>
			<input type="text" name="${pns}firstName" value="$!{currentFirstName}" />
		</td>
	</tr>
	<tr>
		<td>Last Name:</td>
		<td>
			<input type="text" name="${pns}lastName" value="$!{currentLastName}" />
		</td>
	</tr>
	<tr>
		<td>Balance:</td>
		<td>
			<input type="text" name="${pns}balance" value="$!{numberTool.format($currentBalance)}" />
		</td>
	</tr>
	</table>

	<br />

	<input type="submit" value="Save" />
	<input type="button" value="Cancel" onclick="self.location = '${renderUrl}'" />
	</form>
#end

<br /><br />
Expando Bank 2 Image

Enjoy!

Mobile, Point of Sale, Kiosk, Terminal Server, Thin Client, the Web and Liferay

Staff Blogs November 21, 2009 By Ray Augé Staff

Do you maintain/use a Mobile delivered App?

Do you maintain/use a Point of Sale delivered App?

Do you maintain/use a Kiosk delivered App?

Do you maintain/use a Terminal Server delivered App?

Do you maintain/use a Thin Client delivered App?

Do you maintain/use a standalone or embeddable Web App or Widget?

 

If you can answer yes to anyone of these questions, have you considered that Liferay can be used in all these scenarios? Probably all at the same time?

In fact, did you know that Liferay has OOTB features which help you to develop such applications?

Oh, we don't have a feature that you require in order to address some application specific concern? We're open source... We or you can add it.

Liferay is not just about aggregation as most other portals are. Liferay is also about delivery.

  • Mobile
  • WSRP
  • Facebook App
  • Google Gadget
  • Net Vibes
  • Embedded HTML Widget
  • Desk-Web App (Chrome, Prism, Air)

It's all there for you. We've done them all.

Using setenv.sh to configure tomcat env AND Debugging

Staff Blogs October 30, 2009 By Ray Augé Staff

I know that some people are suggesting using the setenv.sh script to control JVM settings for tomcat.

Notably I see adding JMX configurations this way, among other things.

Be careful what you add to this script as some things, like JMX settings, will break the ability to use the shutdown scripts.

The setenv.sh script is called regardless of startup or shutdown of tomcat. If you add JMX for instance, it tries to bind the JVM's JMS server to a port. While this sounds ok, during shutdown you don't need this, plus it will cause the JVM you're starting up to pooch because the port is already bound by another JVM (the one you're trying to shutdown).

Also, simply adding these settings to catalina.sh can cause the same problem.

The solution is to isolate things you only want tomcat to have during startups. You can use this if block, right around line 188 of catalina.sh:

...
if [ "$1" != "stop" ] ; then
  JAVA_OPTS="$JAVA_OPTS -Xms1024m -Xmx1024m -XX:PermSize=128m -XX:MaxPermSize=256m -Xshare:off"
  JAVA_OPTS="$JAVA_OPTS -Dsolr.solr.home=/home/some/deploy-sp/solr-1.3"
  JAVA_OPTS="$JAVA_OPTS -Dexternal-properties=/home/some/portal-web/docroot/WEB-INF/src/portal-developer.properties"
  JAVA_OPTS="$JAVA_OPTS -Dcom.sun.management.jmxremote=true"
  JAVA_OPTS="$JAVA_OPTS -Dcom.sun.management.jmxremote.port=9030"
  JAVA_OPTS="$JAVA_OPTS -Dcom.sun.management.jmxremote.authenticate=false"
  JAVA_OPTS="$JAVA_OPTS -Dcom.sun.management.jmxremote.ssl=false"
fi

# ----- Execute The Requested Command -----------------------------------------
...

That should allow the shutdown scripts to work again as expected.

Oh, one more thing... you don't have to add debug startup parameters to tomcat's startup scripts BTW.

i.e. -agentlib:jdwp=transport=....

Tomcat already provides a mechanism to startup up so you can connect with a debugger.

Simply start with catalina.sh instead like so:

$ ./catalina.sh jpda start

The JVM is now running with appropriate debug configuration at port 8000 by default.

Can Liferay & COSS make you smarter?

Staff Blogs October 20, 2009 By Ray Augé Staff

I've recently been reading Pragmatic Thinking & Learning: Refactor Your Wetware by Any Hunt, Pragmatic Bookshelf, and it has really got me thinking... (Yes, that was pretty lame. HaHaHa!)

Actually it got me thinking on "context", "perspective", "metaphors", and in particular the definition of "Pair programming", which struck me as interesting. Pair programming is a highly effective technique used to improve the productivity and quality of work generated by the pair. As Mr. Hunt demonstrates in his book, it benefits from cognitive side effects that may in fact cause a person to be a little bit smarter when working as a pair rather than when working on your own.

Here is the definition as found on WikiPedia:

 

"Pair programming is a software development technique in which two programmers work together at one work station. One types in code while the other reviews each line of code as it is typed in. The person typing is called the driver. The person reviewing the code is called the observer or navigator... While reviewing, the observer also considers the strategic direction of the work, coming up with ideas for improvements and likely future problems to address. This frees the driver to focus all of his or her attention on the "tactical" aspects of completing the current task, using the observer as a safety net and guide."

 

While that definition doesn't mention anything about causing one to be any smarter, what I find stricking is that it does mention things like "considers the strategic direction of the work, coming up with ideas for improvements and likely future problems to address", and "focus all of his or her attention on the "tactical" aspects of completing the current task" which I would quite confidently define as working smarter any day of the week.

The next thing that I find stricking is how closly it resembles a trend in software development which we call "Commercial Open-Source" . How do these two things relate?

Let's re-write the definition and only change a few of key words:

 

"Commercial Open-Source is a software development practice in which two entities work together on one repository of source code. One types in code while the other reviews each line of code as it is typed in. The entity typing is called the company. The entity reviewing the code is called the community. While reviewing, the community also considers the strategic direction of the work, coming up with ideas for improvements and likely future problems to address. This frees the company to focus all of its attention on the "tactical" aspects of completing the current task, using the community as a safety net and guide."

 

This sounds relatively accurate and makes that Pair programming is a pretty great metaphor for Commercial Open Source.

What does that mean exactly?

It might mean that while working on Commercial Open Source projects, like Liferay, the community is a little bit smarter, the company is a little bit smarter. It might mean than Commercial Open Source projects, like Liferay, are a little bit better for it.

It also means that it's in the best interest of both company and community to ensure and nurture a lasting relationship such that the pair can continue to be effective and continue to be smarter.

Until next time, let's all get smarter together!

Rule #57 of Liferay Theme Design (for non-theme guys like me)

Staff Blogs October 19, 2009 By Ray Augé Staff

Have you ever had a super freakish problem with the Liferay CSS Not doing what you expected? I mean I love Liferay's new Minifier, it's fast and efficient. But, it's no fun when you have problems.

Suppose you have some css:

.something-a .something-b,
.something-c .something-d,
.ie6 .something-e {
  ... do this ...
}

.something-g {
  ... do that ...
}

Looks innocent enough, right?

NOPE!

 

It's not innocent! Inside there lies a develish beast that is waiting to bite your earlobes off. But really he's supposed to be your friend... he just needs to be handled with certain amount of firmness, tough love so to speak. You have to set and keep some rules. Otherwise, he'll misbehave.

He'll reduce the above css on a non-ie6 browser to this:

.something-a .something-b,
.something-c .something-d,
.something-g {
  ... do that ...
}

which is NOT what you wanted at all, am I right?

Rule #57 of Liferay Theme Design is: Never, ever, ever mix browser css selectors with non-browser selector css definitions! Always break them out like this:


.something-a .something-b,
.something-c .something-d
{
  ... do this ...
}


.ie6 .something-e {

  ... do this ...
}

.something-g {
  ... do that ...
}

There, now you have laid down the law! Mr. Minifier will handle your css appropriately.

Adding Expandos from a Startup Hook

Staff Blogs October 9, 2009 By Ray Augé Staff

I've seen a tone of requests lately for how to add Expando Tables and Columns programatically during some startup proceedure.

The biggest problem seems to be with PermissionChecker. The problem stems from the fact that alot of people are trying to use the ExpandoBridge API to create columns (attributes) during the startup process. The issue with that is that the default implementation of ExpandoBirdge has permission checking built in. This leads to exceptions because there is no PermissionChecker yet bound to the thread during startup.

Have no fear. There is a solution! And, it was written by none other than Brian "The Man" Chan himself, so it's gotta be right. He did this in the WOL portlet plugin that you all know from Liferay.com, so it's even in production.

Here it is verbatim from the 5.2.x branch in SVN:

package com.liferay.wol.hook.events;

import com.liferay.portal.kernel.events.ActionException;
import com.liferay.portal.kernel.events.SimpleAction;
import com.liferay.portal.kernel.util.GetterUtil;
import com.liferay.portal.model.User;
import com.liferay.portlet.expando.DuplicateColumnNameException;
import com.liferay.portlet.expando.DuplicateTableNameException;
import com.liferay.portlet.expando.model.ExpandoColumnConstants;
import com.liferay.portlet.expando.model.ExpandoTable;
import com.liferay.portlet.expando.service.ExpandoColumnLocalServiceUtil;
import com.liferay.portlet.expando.service.ExpandoTableLocalServiceUtil;

/**
 * <a href="StartupAction.java.html"><b><i>View Source</i></b></a>
 *
 * @author Brian Wing Shun Chan
 *
 */
public class StartupAction extends SimpleAction {

	public void run(String[] ids) throws ActionException {
		try {
			doRun(GetterUtil.getLong(ids[0]));
		}
		catch (Exception e) {
			throw new ActionException(e);
		}
	}

	protected void doRun(long companyId) throws Exception {
		setupExpando();
	}

	protected void setupExpando() throws Exception {
		ExpandoTable table = null;

		try {
			table = ExpandoTableLocalServiceUtil.addTable(
				User.class.getName(), "WOL");
		}
		catch (DuplicateTableNameException dtne) {
			table = ExpandoTableLocalServiceUtil.getTable(
				User.class.getName(), "WOL");
		}

		try {
			ExpandoColumnLocalServiceUtil.addColumn(
				table.getTableId(), "jiraUserId",
				ExpandoColumnConstants.STRING);
		}
		catch (DuplicateColumnNameException dcne) {
		}

		try {
			ExpandoColumnLocalServiceUtil.addColumn(
				table.getTableId(), "aboutMe", ExpandoColumnConstants.STRING);
		}
		catch (DuplicateColumnNameException dcne) {
		}
	}

}

Now that is some sweet code!

One small note. These columns were added to a table called "WOL". As such these are NOT what we ferrer to as Custom Attributes.

There needs to be one small change made in order for the ExpandoColumns above to be considered Custom Attributes. They must be added to a special table called "DEFAULT_TABLE" (a.k.a. ExpandoTableConstants.DEFAULT_TABLE_NAME).

		try {
			table = ExpandoTableLocalServiceUtil.addTable(
				User.class.getName(), ExpandoTableConstants.DEFAULT_TABLE_NAME);
		}
		catch (DuplicateTableNameException dtne) {
			table = ExpandoTableLocalServiceUtil.getTable(
				User.class.getName(), ExpandoTableConstants.DEFAULT_TABLE_NAME);
		}

Columns in this "DEFAULT_TABLE" are the ones which are retreived and manipulated by the ExpandoBridge API (more on that API in another blog post).

G[rease]Mail

Staff Blogs May 31, 2009 By Ray Augé Staff

Well, I decided this week, while I was "in the field" that I would give GMail a try. I've had an account for a long time now, but I'm used to having everything local on a machine where I store gigs and gigs of mail from at least the past 8-9 years.

I'm a email pack-rat I guess.. but it has saved my butt more than once.

But, it's hard to carry around a desktop just because it has all my email.. and it's hard to not do email when you're not near that desktop. I had to bite the bullet and do something radical like resolve myself to permanently use a remote mail service, namely GMail (I also have Fusemail, which sadly I long ago suggested we use for our work email platform, and has not been the astounding success it was supposed to be).

So GMail, here I come. First things first. GMail is far from perfect, but it has enough features to suite my needs. My biggest complaint was with wasted realestate; bad column widths, font sizes, ads, etc.

Luckily I only ever use Firefox, so "Greasemonkey to the rescue". I installed the plugin and started hacking and testing with firebug to locate the offending elements.

As a result I produced the G[rease]Mail greasemonkey user script, the result of which is the start of a nice clean GMail UI:

 

 Now, maybe I can start to like GMail a little more.

My simple greasemonkey GMail UI cleanup script: G[rease]Mail

Enjoy!

Loving the new machine...

Staff Blogs April 30, 2009 By Ray Augé Staff

Well, over the last couple years I've been largely working from a laptop. I thought I liked it... until recently I simply needed more power.

I went ahead and did some research and found that there was an amazing deal out there that was simply too good to pass up.

The Dell Studio XPS, featuring the new Intel i7 series processor. This thing is simply amazing. It features:

  • 8 cores @ 8MB L2 Cache, 2.66GHz
  • 12GB Tri-Channel DDR3 SDRAM at 1066MHz

 

Another look.

I hate to sound like I'm bragging about this... I was always years behind the bleeding edge when it came to hardware. It's the first time I've treated myself to a decent machine, and I'm very happy.

To anyone looking for a high performance at a fairly low price point, get one of these... for less that $2k CDN it's a steel.

Patents, crimes against humanity!

Staff Blogs March 16, 2009 By Ray Augé Staff

[PS: this is my personal opinion and should not reflect in anyway the great company I work for.]

Quite often we hear the saying that a person is the product of their environment. We all know that everyone is unique and that this uniqueness boiled together with the events and experiences of a lifetime which make for truely unique individuals.

That being said, I often ponder these:

1) Are ideas our own? Truely and fully our own? They are influenced by eventhing around us. The same experiences that shape us and help mold our character are what help us form ideas. So, doesn't that mean that you share a little bit of the ownership of an idea with everyone and everything that caused you to develop it? Without them you likely would not have perceived it to begin with.

2) If you have an idea which can better the world around you, and you do not share it, are you steeling from those who helped you get to the point where you perceived it? If the great scientists of the past had not shared their ideas, where would our world be today?

I see patents as a form of theft against nature, against humanity. When a person claims sole ownership of, and imposes restrictions on an idea they are stating that they have not been influenced by the world in any way, they are deliberately making an attempt to subvert the natural progress of humanity by limiting the impact the idea can have on the next generation of thinkers who they themselves must be influenced by the world around them. This next generation is crippled in the sense that they have been physically restricted from venturing down a stream of thought which in some way expands of the original idea. They are in reality saying "Don't even think about it!" and meaning it in the truest sense.

I would never take a way the credit one deserves for the ideas they perceive, but it's simply dishonest to think they perceived them solely on their own. Thus any patent is a crime against humanity as it is solely designed to inhibit its use by others. Though perhaps not the goal of patents, their result is to deliberately impose further disabilities on humanity. We already have enough of those.

Deploying plugins to Liferay

Staff Blogs March 13, 2009 By Ray Augé Staff

Alot of people seem to have problems deploying custom plugins to liferay. I admit that deployment can be a little tricky.

First off, Liferay has a naming convention which is more than just a naming convention and is actually designed to impose behavior. I think that in the future this should be changed to be based strictly on metadata contained in descriptors.

Anyway, that doesn't help you right now. So, plugins come in 5 types:

  • hooks
  • layout templates
  • portlets
  • themes
  • webs

So, when creating plugins projects the naming of your project dir should be:

project-name-{type-sufix}

where {type-sufix} is one of

  • hook
  • layouttpl
  • portlet
  • theme
  • web

This is done automatically when using the ant tasks or create scripts in the SDK. But if you created your project from scratch but still using the liferay build scripts, then this is where you fall into the mis-naming trap.

If you don't follow this convention one potential side effect is that you end up with a deployed project with a context like:

webapps/project-name-5.1.3.7

instead of just

webapps/project-name

This is because Liferay doesn't recognize the "type" of plugin and assumes it's a plain old webapp and doesn't change the context path.

Now, sometimes your code is already written OR you are including something inside the plugin that WANTs to use a specific context path. How do you deal with that? Liferay jas a little known feature to handle this. Simply add the following in the liferay-plugin-package.properties file.

recommended-deployment-context={desired-context-name}

This value will be used as the context path of the deployed plugin.

Enjoy!

Showing 21 - 40 of 69 results.
Items 20
of 4