Monday, November 29, 2010

The Ghost EL of JSF and Liferay

Yes, this one is a nightmarish tale of woe, of pain, of suffering and of a desk with a vague dent in it shaped roughly like a forehead...

The scenario:

You're deploying a portlet into Liferay with Tomcat that's built with some flavor of JSF or another and when you watch the output during the deploy you see a message that says something like this:

java.lang.LinkageError: loader constraint violation: when resolving interface method "javax.servlet.jsp.JspApplicationContext.getExpressionFactory()Ljavax/el/ExpressionFactory;" the class loader (instance of org/apache/catalina/loader/WebappClassLoader) of the current class, com/sun/faces/config/ConfigureListener, and the class loader (instance of org/apache/catalina/loader/StandardClassLoader) for resolved class, javax/servlet/jsp/JspApplicationContext, have different Class objects for the type javax/el/ExpressionFactory used in the signature

Now, in Googling this beast of an error you will see a lot of very helpful advice that usually revolves around "Take the el-api.jar file out of your WEB-INF/lib folder in your project."

See, the conflict arises when Tomcat already has an el-api.jar file in its lib folder so when you try to bring yours to the party it causes a collision.

"But wait!" You say. "I did that! I went into my pom.xml and made sure to exclude that dependency so now it doesn't show up in my .war file and still I get the error!!!!!!!111111111"

So you've verified that there's no sign of that el-api.jar file in your .war file's WEB-INF/lib folder? You sure?

Yes? ok then here's what you do next:

Go to your webapps folder in Tomcat. see your portlet folder in there? Open it. Now go into the WEB-INF/lib folder inside it.

Yeah. You see it, don't you?

See, back when you deployed your portlet the first time and put that el-api.jar file there, it caused the initial problem. Later, when you re-deployed your project without that .jar, it got deployed but it didn't completely blow away the old installation. That's why it's best to make sure it's gone when you re-deploy. How? Just delete the portlet's folder from webapps and wait a minute or two. Liferay will notice it's gone then unregister the portlet. You can now safely deploy your portlet again.

Thursday, November 18, 2010

That Video is Too Small!

So your portlet that shows videos works great, you keep replaying it over and over 'cause you're proud of it, and your boss thinks it's pretty cool except...

"... it's so small... Can't really even see those Flash controls. Any way to have the user be able to make it bigger, like maybe full screen sized as when they click to maximize the portlet? That way it can be small and out of the way when they aren't watching it!"

Short answer: Yes.

In Liferay you can maximize a portlet to make it easier to see or more usable. The problem is maximizing the portlet makes the PORTLET bigger but that Flash video isn't going to grow with it. What you need is code that will cause the video to grow with the portlet.

Fortunately, this IS possible. It's easy, too.

First, take a look at your HTML code for your object. (In this case, it's an ice:outputMedia control.) If you don't have it already you're going to need a style attribute to set the width and height of your video. Usually it looks something like style="width: 300px; height: 300px;"

If you don't have that, add it. If you have it, change it to look like this:

style="width: #{videoDisplay.width}px; height: #{videoDisplay.height}px;"

Yes, we're going to do this with JSF/ICEfaces. If you're not using that tech then just substitute whatever JSP tag you want that'll make those values dynamic.

Next, you're going to need to add some members to your backing bean.

private int width, height;
private boolean maximized;
private static int MAX_WIDTH = 900;
private static int MAX_HEIGHT = 900;
private static int MIN_WIDTH = 300;
private static int MIN_HEIGHT = 300;

and some setters and getters for those non-static ints.

public void setMaximized(boolean maximized) {
if(maximized){
width = MAX_WIDTH;
height = MAX_HEIGHT;
}
else{
width = MIN_WIDTH;
height = MIN_HEIGHT;
}
this.maximized = maximized;
}

public boolean isMaximized() {

return maximized;
}

public void setWidth(int width) {
this.width = width;
}

public int getWidth() {
return width;
}

public void setHeight(int height) {
this.height = height;
}

public int getHeight() {
return height;
}

"But wait!" You say. "There's actual processing being done in your setter! Isn't that bad practice?"

Some say it is, some say it isn't. Know what I say? Whatever makes the code cleaner and simpler. You'll see the method (pardon the pun) to my madness in a moment.

Make sure you initialize your width and height too, by the way. Either put it in your backing bean's constructor or init() method or wherever you set things up.

if(this.isMaximized()){
width = MAX_WIDTH;
height = MAX_HEIGHT;
}
else{
width = MIN_WIDTH;
height = MIN_HEIGHT;
}

Now, the tricky part is that we want the maximization to be determined before the page renders. That means you're going to need to build a phase listener class if you don't have one already. Here's a basic one:

package com.myproject.listener;

import javax.faces.context.ExternalContext;
import javax.faces.context.FacesContext;
import javax.faces.event.PhaseEvent;
import javax.faces.event.PhaseId;
import javax.faces.event.PhaseListener;
import javax.faces.render.ResponseStateManager;
import javax.portlet.PortletPreferences;
import javax.portlet.RenderRequest;
import javax.portlet.WindowState;

public class VideoPhaseListener implements PhaseListener {

private static final long serialVersionUID = 1L;

public void afterPhase(PhaseEvent pe) {
//some implementation
}

public void beforePhase(PhaseEvent pe) {

VideoDisplay videoDisplay = (VideoDisplay) facesContext
.getApplication().getELResolver().getValue(
facesContext.getELContext(), null, "videoDisplay");

if (pe.getPhaseId() == PhaseId.RENDER_RESPONSE) {
FacesContext facesContext = FacesContext.getCurrentInstance();
ExternalContext externalContext = facesContext.getExternalContext();
RenderRequest renderRequest = (RenderRequest) externalContext
.getRequest();
PortletPreferences prefs = renderRequest.getPreferences();
videoDisplay.setMaximized((renderRequest.getWindowState().equals(WindowState.MAXIMIZED)));
}
}

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

See what's happening? Before the Render Response phase we're getting the portlet state from the Render Request. So if the user has clicked the maximize button, the state is MAXIMIZED and we want the backing bean to use the maximized constant values we set up earlier. Otherwise we want the minimums.

See now why our setMaximized() setter had that bit of processing? When we change the value of maximized in the backing bean we want to immediately change the width and height to match. Could I have used the setWidth() and setHeight() methods here and set them all from the Phase Listener? Well yeah I could have but this way is cleaner and makes the backing bean (videoDisplay) manage itself. It isn't the job of the Phase Listener to handle the details of what goes on in the backing bean. Its job here is to report the state of the portlet. Period.

One last step: Make sure you configure your new listener in faces-config.xml.

<lifecycle>
<phase-listener>
com.myproject.listener.VideoPhaseListener
</phase-listener>
</lifecycle>

So when the page response is rendered, it'll use the values appropriate for the window state.

Now, you could take this a step further and have those static width and height values come in from some sort of configuration and avoid hard coding, but you get the idea.

Monday, November 15, 2010

I Want to Watch Videos in Liferay! Part II

So you've uploaded your videos into the Liferay 5.2 Document Library but now what? How to view them?

Well, one way is to build a portlet.

A little caution here. The video format you choose is going to impact what happens when you try and view it. Different browsers handle this stuff in different ways. One browser might play your content in the designated video viewer while another treats it like a file download. Your best bet is to use a viewer component that plays Flash videos, and stick to those.

So what now?

Choose a component that will display your video. The simplest way is to embed the video object like this:

<object type="application/x-shockwave-flash" style="width: 200px; height: 200px;" data="http://someurl"/>

And there you go.

You can also get a little fancier and use a control like ICEsoft's outputMedia control. It's a little more complicated but you can do a lot more with it programmatically. You can see how it works by going to the ICEfaces website and looking at the Component Showcase.

Then your view page will have markup that looks a little like this:

<ice:outputMedia
id="outputMedia"
player="flash"
source="#{videoDisplay.source}">
<f:param name="play" value="true"/>
</ice:outputMedia>

Again, be aware of your browser and format environment. Choose "windows" as your player to view .wmv files and it'll work great in Internet Explorer, but try it in Chrome and you'll get a download instead. As I said before I strongly recommend Flash here.

The problem is, of course, to know what to put in that source URL. Notice in this second example instead of a hard-coded URL for the source I'm using a JSF reference to a String stored in my backing bean. Why? Because I'm generating the source URL dynamically, based on some configuration options selected by the user. You could hard code a URL in there if you wanted. Personally, I despise hard coding anything. (You could also use a JSF reference for the simpler embedded video object above.)

So how to get at that file?

When you request a document from a Liferay Document Library the URL has to be of a specific structure. Here's an example:

http://localhost:8080/c/document_library/get_file?uuid=67874de5-d480-45d6-82dd-610e9d1a783e&groupId=10136

If you're interested, you can find this value by going into your Document Library portlet, clicking the "Actions" button, and selecting "View." It will display the actual URL of that file.

You can generate this URL programmatically by using the Liferay Service Layer. Here's a simple method to grab those details:

private String getUrl(long selectedFileId){

DLFileEntry file = com.liferay.portlet.documentlibrary.service.DLFileEntryLocalServiceUtil.getFileEntry(selectedFileId);

return "http://" + hostname + "/c/document_library/get_file?" +
file.getUuid() + "&groupId=" +
file.getGroupId();
}


Notice the hostname isn't hard coded either. That should be a configuration option unless you plan to deploy this portlet on only one server, ever.

"Great!" You say. "But how do I get the file id?"

Well, one way is to browse the folders in the Document Library. Suppose we wanted to choose a video file from a folder in the Document Library named "Previews"

List folderSet;
long selectedFolderId;
FacesContext facesContext = FacesContext.getCurrentInstance();
ExternalContext externalContext = facesContext.getExternalContext();
RenderRequest renderRequest = (RenderRequest) externalContext .getRequest();

long companyId = PortalUtil.getDefaultCompanyId();
(Make sure this goes in a try-catch block.)

folderSet = com.liferay.portlet.documentlibrary.service.DLFolderLocalServiceUtil.getFolders(companyId);
folders = new ArrayList();
for(DLFolder folder : folderSet){
if(folder.getName() .equals("Previews")){
selectedFolderId = folder.getFolderId();
}


Now that you've got the folder you want, you can get the details from each file in it. Let's say we know the name of our video file is "trebuchet.swf" and it's in the folder we found above.

(Make sure this goes in a try-catch block too.)

String videoName = "trebuchet.swf";

DLFileEntry video = com.liferay.portlet.documentlibrary.service.DLFileEntryLocalServiceUtil.getFileEntry(selectedFolderId, videoName);

now you can use that DLFileEntry object to get the file Id to pass into the method we created above:

getUrl(video.getFileEntryId());

And there you have it. Make sure your component can access it through your backing bean. Maybe initialize your bean with all that code and finish with something like this:

this.source = getUrl(video.getFileEntryId());

Now, I'm not touting this as the very best possible way to do it, and if anybody out there has a better idea please, pass it along. This works, though. So it'll get you results.

In my video portlet, I used ice:selectOneMenu controls to let the user browse the folders in the Document Library, then choose the video file they'd like to view.

Wednesday, November 10, 2010

I Want to Watch Videos in Liferay! Part I

So you want to create a portlet that runs in Liferay 5.2 that will play videos stored in your Document Library.

There are a series of hurdles that you'll have to overcome to be able to do this, however. Nothing too horrible, but it's easy to underestimate what a pain it can be.

First, Liferay is kind of picky about what kinds of files can be uploaded into it. By default it won't accept videos of certain formats and the file size is fairly limited anyway. Luckily, this is an easy fix.

Do you have a portal-ext.properties file in your Liferay home directory? No? Create one. If you don't know what this file is for, it's a configuration file that will override the settings that are in Liferay by default. Create the empty file, name it portal-ext.properties, and add the following line to it:

dl.file.max.size=3072000

This will give you more maneuvering room for uploading videos. You can set that to whatever value is best for your project.

Next, you have to tell Liferay what file formats to accept. Add this line:

dl.file.extensions=.bmp,.css,.doc,.dot,.gif,.gz,.htm,.html,.jpg,.js,.lar,.odb,.odf,.odg,.odp,.ods,.odt,.pdf,.png,.ppt,.rtf,.swf,.sxc,.sxi,.sxw,.tar,.tiff,.tgz,.txt,.vsd,.xls,.xml,.zip,.avi,.mov,.wmv

Make sure you have no carriage returns in that file if you're copying and pasting this! If you wind up with newlines in that parameter list it'll ignore everything after it.

Now restart Liferay.

You should now be able to upload video files with the above formats into your Document Library.

Not too bad, eh?

Monday, November 8, 2010

JavaScript errors in IE with ICEfaces and Liferay.

JavaScript errors in Internet Exploerer?!?!?!? Surely not!

Yes, friends. It is my sad duty to report that yes indeed, Internet Explorer may have JavaScript issues when using ICEfaces in Liferay when Firefox and Chrome do not.

But then, if you found this blog because you Googled that, you already knew it, and now you'd like me to stop gabbing and tell you the solution.

Oh I get it.

So here's the scenario:
Liferay 5.2
ICEfaces 1.8.1

You create some nifty little portlet that installs into Liferay fine and displays fine and... uh oh... some of the functionality doesn't work and you get that irritating little icon in the bottom left corner of IE telling you there's a JavaScript error on the page. Clicking it to show the error gets you nothing but a message telling you "Error: Object expected" and what line the error occurred on.

Useless, ain't it?

So here's the thing: According to this post:

http://www.liferay.com/community/forums/-/message_boards/message/5441603#_19_message_3413693

Liferay and ICEfaces compress .js files and this does not make Internet Explorer happy. If you want your portlet to run in IE, you have to disable this feature.

Go into your web.xml file and add the following:

<context-param>
<param-name>com.icesoft.faces.compressResources</param-name>
<param-value>false</param-value>
</context-param>

And try your app again. That hideous little error should be gone.