Fork me on GitHub

n. Slang a rough lawless young Kuali developer.
[perhaps variant of Houlihan, Irish surname]
kualiganism n

Blog of an rSmart Java Developer. Full of code examples, solutions, best practices, et al.

Friday, April 30, 2010

REPOST KFS Inheritance/Composition with Private Methods

Why the Repost?

I got some feedback that I
  • Never resolved the namespace issue
  • Didn't show original code to explain the problem
Both are very good points, so I'm going to do that now.

The Problem Scenario

Suppose an instance arrives where you need to change the functionality of KFS slightly, but the method you want to override is declared private. Need an example? I have one. Read on.

Digester and Namespace Validation

By default, Digester within KFS has namespace validation off. Suppose you want to turn it on. Now you run into exactly such issue. Below is the original code from the XmlBatchInputFileTypeBase
...
...
public class XmlBatchInputFileTypeBase extends BatchInputFileTypeBase {
/**
* @see org.kuali.kfs.sys.batch.BatchInputFileType#parse(byte[])
*/
public Object parse(byte[] fileByteContent) throws ParseException {
if (fileByteContent == null) {
LOG.error("an invalid(null) argument was given");
throw new IllegalArgumentException("an invalid(null) argument was given");
}

// handle zero byte contents, xml parsers don't deal with them well
if (fileByteContent.length == 0) {
LOG.error("an invalid argument was given, empty input stream");
throw new IllegalArgumentException("an invalid argument was given, empty input stream");
}

// validate contents against schema
ByteArrayInputStream validateFileContents = new ByteArrayInputStream(fileByteContent);
validateContentsAgainstSchema(getSchemaLocation(), validateFileContents);

// setup digester for parsing the xml file

Digester digester = buildDigester(getSchemaLocation());

Object parsedContents = null;
try {
ByteArrayInputStream parseFileContents = new ByteArrayInputStream(fileByteContent);
parsedContents = digester.parse(validateFileContents);
}
catch (Exception e) {
LOG.error("Error parsing xml contents", e);
throw new ParseException("Error parsing xml contents: " + e.getMessage(), e);
}

return parsedContents;
}

}

The problem is with buildDigester(getSchemaLocation()). XmlBatchInputFileTypeBase#buildDigester() is declared private. That is, if one tries to extend XmlBatchInputFileTypeBase, you not only cannot override the buildDigester() method, but you can't call it from an inheriting class either.

We would love to do
        Digester digester = buildDigester(getSchemaLocation());

But we can't. The alternative is to use reflection and call the private buildDigester method anyway.

Here's an example:
package edu.arizona.kfs.sys.batch;
...
...
public class XmlBatchInputFileTypeBase extends org.kuali.kfs.sys.batch.XmlBatchInputFileTypeBase {
/**
* @see org.kuali.kfs.sys.batch.BatchInputFileType#parse(byte[])
*/
public Object parse(byte[] fileByteContent) throws ParseException {
if (fileByteContent == null) {
LOG.error("an invalid(null) argument was given");
throw new IllegalArgumentException("an invalid(null) argument was given");
}

// handle zero byte contents, xml parsers don't deal with them well
if (fileByteContent.length == 0) {
LOG.error("an invalid argument was given, empty input stream");
throw new IllegalArgumentException("an invalid argument was given, empty input stream");
}

// validate contents against schema
ByteArrayInputStream validateFileContents = new ByteArrayInputStream(fileByteContent);
validateContentsAgainstSchema(getSchemaLocation(), validateFileContents);

// setup digester for parsing the xml file

Digester digester = null;
try {
Method digesterMethod = getClass().getDeclaredMethod("buildDigester", String.class, String.class);
m.setAccessible(true); // Private? Who cares?! I sure don't.
m.invoke(this, getSchemaLocation(), getDigestorRulesFileName());
}
catch(IllegalAccessExption e) {
// Not since it's accessible now.
}
catch(InvocationTargetException e) {
// Something else naughty happened
}

if (digester == null) {
// throw some exception because this is very bad
}

digester.setNamespaceAware(true); // Enabling the namespace checking! Yeay!


Object parsedContents = null;
try {
ByteArrayInputStream parseFileContents = new ByteArrayInputStream(fileByteContent);
parsedContents = digester.parse(validateFileContents);
}
catch (Exception e) {
LOG.error("Error parsing xml contents", e);
throw new ParseException("Error parsing xml contents: " + e.getMessage(), e);
}

return parsedContents;
}

}

The trick is in

Digester digester = null;
try {
Method digesterMethod = getClass().getDeclaredMethod("buildDigester", String.class, String.class);
m.setAccessible(true); // Private? Who cares?! I sure don't.
m.invoke(this, getSchemaLocation(), getDigestorRulesFileName());
}
catch(IllegalAccessExption e) {
// Not since it's accessible now.
}

By calling m.setAccessible(true), we can still call the private method. many call this a hack because it ignores the private access. I think many people are confused in what that means. The control is rather just for managing scope and maintaining OO encapsulation. These directives are not intended for security. They are intended to enforce OO. If we wanted to restrict method access, a SecurityManager implementation would be more appropriate. IMHO, this is an entirely acceptable thing to do in this situation. That being that we are being restricted by a flaw in the API from doing something we should be allowed to do. This happens often, and is a much better alternative than duplicating code.

Then later, we append our changes to the digester with...

digester.setNamespaceAware(true); // Enabling the namespace checking! Yeay!

With this, we can get away with extending code and overriding behavior (setting namespace awareness).

No comments:

Post a Comment