Sunday, February 22, 2009

Getting Struts 2.1 to work in Weblogic 10.3

Over the last couple of days, I've been looking into the problems of running Struts2 in Weblogic 10.3. Once you wade through the errors however, the resolution is not as complicated. So, the main reason why Struts 2.1 does not work in Weblogic 10.3 (and could be earlier versions) is mainly because of the way Weblogic handles the WEB-INF classes and lib directories. The classes directory is zipped up into a jar called _wl_cls_gen.jar. Struts 2.1 specifically is looking for actions either in the classes directory or in a jar with a "META-INF/Manifest". Unfortunately, the jar created does not have the manifest and Struts2 does not know that it needs to look in a jar for the struts actions. Also, every jar within a WAR file is available as a resource via the zip file protocol, instead of a more standard jar file protocol (a really quirky implementation from BEA on this....:(, shame on them!!!).

So then how does one go about fixing these issues? Here are the steps:

  • You will need to add the META-INF/Manifest file into the classes directory of your WAR. This will guarantee that the _wl_cls_gen.jar is a valid jar that Struts2 will recognize.

  • You will then need to add a struts.xml to your project with the following information:
    <constant name="struts.convention.action.includeJars" value=".*_wl_cls_gen.*"/>
    Note here that with Struts 2.1, you will be using the struts2-convention plugin, and this is an override for one of the constants that the convention plugin uses to look for actions.

  • So far, these steps were pretty easy. Now to a little bit of coding work. You will need to change a small piece of code in the convention plugin. There are 2 pieces. One piece is the change in the plugin's struts-plugin.xml. In talking with Musachy Barroso, a lead developer on Struts, this change has already been committed to the trunk. This change is to allow the convention plugin to accept the zip protocol. The change in the struts-plugin.xml is:
    <constant name="struts.convention.action.fileProtocols" value="jar,zip" />

    The next is the following piece of code in the org.apache.struts2.convention.PackageBasedActionConfigBuilder class:


    private Set fileProtocols;

    @Inject("struts.convention.action.fileProtocols")
    public void setFileProtocols(String fileProtocols) {
    if (!StringTools.isTrimmedEmpty(fileProtocols)) {
    this.fileProtocols = TextParseUtil.commaDelimitedStringToSet(fileProtocols);
    }
    }

    private UrlSet buildUrlSet() throws IOException {
    UrlSet urlSet = new UrlSet(getClassLoader());

    urlSet = urlSet.exclude(ClassLoader.getSystemClassLoader().getParent());
    urlSet = urlSet.excludeJavaExtDirs();
    urlSet = urlSet.excludeJavaEndorsedDirs();
    urlSet = urlSet.excludeJavaHome();
    urlSet = urlSet.excludePaths(System.getProperty("sun.boot.class.path", ""));
    urlSet = urlSet.exclude(".*/JavaVM.framework/.*");

    if (includeJars == null) {
    urlSet = urlSet.exclude(".*?jar(!/)?");
    } else {
    //jar urls regexes were specified
    List rawIncludedUrls = urlSet.getUrls();
    Set includeUrls = new HashSet();
    boolean[] patternUsed = new boolean[includeJars.length];

    //- Changed section begins
    for (URL url : rawIncludedUrls) {
    if (fileProtocols.contains(url.getProtocol())) {
    //it is a jar file, make sure it macthes at least a url regex
    for (int i = 0; i < includeJars.length; i++) {
    String includeJar = includeJars[i];
    String extUrlForm = url.toExternalForm();
    if (Pattern.matches(includeJar, extUrlForm)) {
    // If the protocol is zip, convert to jar protocol
    if ( extUrlForm.indexOf("zip:")!=-1 ) {
    String newUrl = "jar:file:/" +
    extUrlForm.substring(4);
    url = new URL(newUrl);
    }
    includeUrls.add(url);
    patternUsed[i] = true;
    break;
    }
    }
    } else {
    //it is not a jar
    includeUrls.add(url);
    }
    }
    //- Changed section ends
    if (LOG.isWarnEnabled()) {
    for (int i = 0; i < patternUsed.length; i++) {
    if (!patternUsed[i]) {
    LOG.warn("The includeJars pattern [#0] did not match any jars in the classpath", includeJars[i]);
    }
    }
    }
    return new UrlSet(includeUrls);
    }

    return urlSet;
    }

    The piece of code that hasn't been committed and in talking with Musachy Barroso is felt temporary is the conversion of zip protocol to jar in the buildUrlSet method. This hasn't been committed to the trunk and most likely will go in as a broader code change that will affect the XWork library that Struts2 uses.


What I've laid out are fairly simple changes - compiling the struts2 convention plugin should be a cakewalk (maven based). I will keep this site updated as I hear more from Musachy if you are willing to wait it out before trying Struts2 in Weblogic. Let me know if you have problems getting things to work....

- Amit

UPDATE:
The above changes are not required with Struts 2.2. Just put the following in the struts.properties file of the project.
#struts convention property modifications for Weblogic
struts.convention.action.includeJars=.*?/your-web-app-name.*?jar(!/)?
struts.convention.action.fileProtocols=jar,zip

Wednesday, February 18, 2009

Struts2 Annotations, Dojo and Json plugin

Here's a nice way to integrate Dojo, json with the latest Struts2.1 web framework. A point to note here is that Dojo support via a plugin is deprecated in Struts2.1. My example does not use the dojo plugin, but the dojo library directly with the help of the json-plugin from the Google repository.

My Struts2 action looks as follows:

@ParentPackage("json-default")
@Namespace("/testing")
public class AjaxTestAction
{
@Action( interceptorRefs={@InterceptorRef(value="json", params={"enableSMD","true"})},
results={@Result(name = "success", type="json", params={"enableSMD","true"})} )
public String smd() {
return "success";
}

@SMDMethod
public String sayHello(String name)
{
return ("Hello " + name);
}
}


My javascript looks as follows:

//load dojo RPC
dojo.require("dojo.rpc.JsonService");

//create service object(proxy) using SMD (generated by the json result)
var service = new dojo.rpc.JsonService("<%=contextPath%>/testing/ajax-test.action");

// execute remote service call
var deferred = service.sayHello('Amit');

deferred.addCallback(function(result) {
alert(result.toString());
});

That's all you need. Cool eh?? One thing to note above is the url to the action - I'm using the convention plugin - but yours could look different. Have fun with Dojo. To me AJAX has never been easier. I'm not a UI guy, but my application is really looking like a RIA with Dojo toolkit and this JSON-RPC communication with the backend Java Struts2 actions.

- Amit

Tuesday, February 10, 2009

Convert a jar to an optional package

Here's the code I promised I would post to convert a jar pulled out of a Maven repository into an optional package. Note here that jars pulled out of a Maven repository have the following example syntax: examplelib-1.0.0.jar. Given this syntax, the following code manipulates the Manifest of the jar to make the jar compatible with the JEE optional package specifications:

ZipFile oldZipFile = new ZipFile(new File(fileName));
Enumeration entries = oldZipFile.entries();

ZipOutputStream out = new ZipOutputStream(new FileOutputStream(
outFile));

// Loop through entries in the existing jar file
while (entries.hasMoreElements())
{
ZipEntry entry = entries.nextElement();
// Put all other files besides the manifest into the converted jar as is
if (entry.getName().indexOf("MANIFEST") == -1)
{
out.putNextEntry(entry);

InputStream inStream = oldZipFile.getInputStream(entry);
// Transfer bytes from the ZIP file to the output file
byte[] buf = new byte[1024];
int len;
while ((len = inStream.read(buf)) > 0)
out.write(buf, 0, len);

inStream.close();
}
else
{
manifest = new OptManifest();
manifest.setExtName(trueFileName);

// Read the manifest
InputStream inStream = oldZipFile.getInputStream(entry);
BufferedReader in = new BufferedReader(
new InputStreamReader(inStream));
StringBuffer buffer = new StringBuffer();

// Read each line of the manifest file to determine its state
boolean extnNameFound = false;
boolean specVerFound = false;
boolean implVerFound = false;

String line = null;
while ((line = in.readLine()) != null)
{
if ( !specVerFound )
specVerFound = ( line.toLowerCase().indexOf("specification-version")!=-1? true: false );

if ( !implVerFound )
implVerFound = ( line.toLowerCase().indexOf("implementation-version")!=-1? true: false );

if ( !extnNameFound )
extnNameFound = (line.toLowerCase().indexOf("extension-name")!=-1? true: false);

boolean hasOptPkgParam = ( specVerFound || implVerFound || extnNameFound? true : false );

// Add lines that are not optional package parameters
if ( line!=null && !line.equals("") && !hasOptPkgParam )
buffer.append(line + "\n");
}

// Add Optional package information to the manifest
String versionToUse = determineVersion(fileName);
if ( versionToUse!=null && !versionToUse.equals("") )
{
// Assign cleaned up specification version
String specVersionToUse = getSpecificationVersion(versionToUse);
buffer.append("Specification-Version: " + specVersionToUse + "\n");
manifest.setExtSpecVersion(specVersionToUse);

// Assign cleaned up implementation version
String implVersionToUse = getImplementationVersion(versionToUse);
buffer.append("Implementation-Version: " + implVersionToUse + "\n");
manifest.setExtImplVersion(implVersionToUse);
}

inStream.close();
in.close();

ZipEntry newManifestEntry = new ZipEntry(entry.getName());
out.putNextEntry(newManifestEntry);

// Transfer bytes from the file to the ZIP file
out.write(buffer.toString().getBytes());
}

// Close the new entry in the new zip file
out.closeEntry();
}
out.close();


Here's how your can derive the specification and implementation versions:

private String getSpecificationVersion(String versionToUse)
{
StringBuffer specVersionToUse = new StringBuffer();
// Regex expression that only allows digits and periods, and a digit cannot
// be preceded by an underscore
String regex = "(?<!_| )[\\.?[0-9]*]";
Pattern pattern = Pattern.compile(regex);
Matcher m = pattern.matcher(versionToUse);
while ( m.find() )
specVersionToUse.append(m.group());

if ( specVersionToUse.charAt(0)=='.')
specVersionToUse = specVersionToUse.replace(0, 1, "");

if ( specVersionToUse.charAt(specVersionToUse.length()-1)=='.')
specVersionToUse = specVersionToUse.replace(specVersionToUse.length()-1, specVersionToUse.length(), "");

return specVersionToUse.toString();
}

private String getImplementationVersion(String versionToUse)
{
String implVersion = versionToUse;
int versionIndex = versionToUse.indexOf('-');

// Get the version starting from the hyphen prior to a digit
String regexPattern = "[0-9]";
while ( versionIndex!=-1 )
{
char charAtIndex = versionToUse.charAt(versionIndex+1);
if ( !Pattern.matches(regexPattern, String.valueOf(charAtIndex)) )
versionIndex = versionToUse.indexOf('-', versionIndex+1);
else
{
implVersion = versionToUse.substring(versionIndex+1);
break;
}
}
return implVersion;
}