Skip to main content

Objects and Strings and the Wrangling Thereof

Posted by ljnelson on August 27, 2007 at 10:49 AM PDT

See if this little scenario sounds familiar.

You're rolling along on some application somewhere, and you've decided to put some information in a Properties file somewhere. You realize that you're beginning to encode a lot of information in a property setting, so much so that you realize that really what you're doing is building up a rather complicated Object. You feel like you've done this before.

Or this one:

You're working on a Swing application, and you need to validate the input from the user. Great, you say, I'll use a JFormattedTextField, and then I'll...I'll...I'll...read the...documentation...which features lots of...hmm...factories...and AbstractFormatters...and still more formats and navigation thingees and...I think I'll go get some coffee.

Or this one:

You're halfway through developing a complicated and enterprisey validation framework and you stop abruptly, realizing that there has to be a better way!

Starting with this blog entry I'd like to cover the many, many different ways to edit, format and build up different kinds of Objects that are provided by the Java platform.

Why should you think about turning Objects into Strings? Or Strings into Objects? Or all the other ways that a user might send input to you?

For me, the answer is that whether you're developing on the desktop or on the Web, you are constantly accepting free-form user input in the form of text. In some cases, you have control over this text, and in other cases you do not, but in all cases you often need to turn that text into things like dates, colors, fonts, java.net.URIs, custom domain objects and the like. Wouldn't it be nice to come up with a standard set of tools that would manage this kind of conversion for you in a pluggable manner? Wouldn't it be even better if most of that heavy lifting were done for you by the base platform? Well, it is.

In this entry, I'll cover java.beans.PropertyEditor—at least I'll cover some of its features—and then will move onto other tools in subsequent entries.

PropertyEditors

The java.beans.PropertyEditor class is quite a powerful beast. Despite the fact that it's heavily used by Spring, Geronimo, JBoss and doubtless a whole host of other reasonably popular open source products, it seems to get lost in the shuffle, and developers often don't know it exists.

A PropertyEditor exists to edit Java bean properties. Or at least that's what the documentation will tell you. But explaining it this way often leads to questions about what a Java bean is, what a property is, oh, doesn't that mean getters and setters and whatnot—all of which is largely irrelevant for understanding what a PropertyEditor actually does.

So, then: a PropertyEditor provides a consistent way for accepting user-supplied data and turning it into an object of a particular class. For now, we'll look at just its text conversion utilities and leave its boatload of other features aside.

Before we dive into the API, we should think about what we're trying to do. So for this example, let's say that we're going to make a PropertyEditor that converts java.net.URI instances into Strings and vice versa. That is a trivial enough example that it should be easy to step through, and a useful enough one that you may find yourself using the resulting PropertyEditor in an actual project.

To start developing a new PropertyEditor, it's most helpful to simply subclass java.beans.PropertyEditorSupport, so that's what we'll do here. For reasons that will become clear later (hint: see the API documentation for java.beans.PropertyEditorManager), you should suffix your property editors with the word Editor. Finally, the API documentation also says that all PropertyEditors must have a public, no-arg constructor:

public class URIEditor extends java.beans.PropertyEditorSupport {
  public URIEditor() {
    super();
  }
}

Next, let's focus on the text conversion methods:

public String getAsText();
public void setAsText(final String text) throws IllegalArgumentException;
public Object getValue();
public void setValue(final Object value);

Actually, before we get there, an overriding thing that is worth remembering is that a PropertyEditor is a stateful object. Granted, if all is going as it should, the state doesn't stick around very long, and technically speaking doesn't even get released back into the wild, but it's there nonetheless. That means the typical flow of using a PropertyEditor for conversion purposes is:

  • Stick a value into the editor.
  • Ask the editor to get you its current value's String representation.

Or, equivalently:

  • Try to set some text into the editor. If this call completes, the editor will have a value ready and waiting for you.
  • Get the value out of the editor that resulted from the conversion process.

So back to the methods. Let's start (as is always good) by implementing the setAsText() method first. Then to be good and defensive and bulletproof, we'll beef up the getValue() and setValue() methods in a moment.

public void setAsText(final String text) {
  // 1
  if (text == null) {
    // null text means a null URI
    this.setValue(null);
  } else {
    // 2
    final String trimmedText = text.trim();
    assert trimmedText != null; // guaranteed by trim() method
    // 3
    if (trimmedText.length() <= 0) {
      // Empty text means a null URI
      this.setValue(null);
    } else {
      // We have a non-null, non-zero-length String.  Let's attempt to turn it into a URI.
      try {
        // 4
        this.setValue(new URI(trimmedText));
      } catch (final URISyntaxException kaboom) {
        // 5
        throw (IllegalArgumentException)new IllegalArgumentException(text).initCause(kaboom);
      }
    }
  }
}

What's going on here? Why is this so complicated? Let's walk the interesting bits of the code. The numbers in the list below should correspond to the numbers in the code above.

  1. I am a defensive programmer, so I tend to overdo this part (in some people's opinion).That is, I always check for nulls, even in situations where it can be "guaranteed" that null will never be supplied or returned. So here, I make the decision that if null is supplied to us, I will interpret that as someone wanting to clear out the value stored by this editor. To do that, we simply set the value to null.
  2. Here, you may do this step or not, depending on your requirements. I trim the supplied text to eliminate leading and trailing whitespace. The assertion step is entirely optional; I use it more for documentation than anything else.
  3. Once the text is trimmed, I can make a quick call to get its length, and if the string is empty, then I interpret that to mean that the user is, again, trying to clear out the value. So I set the value to null. I suppose you could be draconian and attempt to construct a URI with an empty string, but I don't really see the point.
  4. By the time we get here, we know we have a "full" String to work with. PropertyEditors always have to return a copy of whatever value is set on them, but in the case of URIs, the URI itself is immutable, so when we call setValue here with a new URI, we will not have to do anything special in our getValue method later on to copy the installed value. More on this later.
  5. The only specification-compliant option you have for rejecting input is to throw an IllegalArgumentException. So here we catch the URISyntaxException, and, using some rather weird syntax, wrap that URISyntaxException inside an IllegalArgumentException. The initCause() call is needed to make this code compile under 1.4 environments. As a convenience, I supply the bad text itself (untrimmed, unprocessed) in the IllegalArgumentException's message.

What's important about the thought process behind implementing this (very simple) method is, I think, the following:

  • Handle all kinds of input. If you do this up front, you won't be surprised later, and your code will perform gracefully under pressure. Can you accept dirty input? Null? What happens if there is whitespace? Can you trim some of it away? Or does that disturb the semantics of your object-to-string conversion? In the case of URIs, there's really no harm in trimming the whitespace fat.
  • Think about providing a way, using just text, to clear out a value. Consider, also, when this might be a bad idea. This PropertyEditor implementation lets you clear the value by calling setAsText with either "" or null.
  • Work within the specification as much as you possibly can. Although of course you're free to throw all sorts of different kinds of RuntimeExceptions whenever you want in a vacuum, the PropertyEditor specification says that the only RuntimeException (really, the only kind of Exception, period) that callers will expect is an IllegalArgumentException. So throw one of those when things go bad. I always make the bad text be the message, because that will show up in a stack trace in various log files. Also, callers of PropertyEditors will almost always catch a simple plain-Jane IllegalArgumentExcepiton, and won't know how to access any other values about it other than its message and cause.

The getAsText() method is much simpler. Its only responsibility is to return the text representation of the currently installed value. But be careful (and now you'll understand why I went to great lengths to trim the text in the setAsText() method above), because the specification says that if you return null from this method it is to be interpreted to mean that your PropertyEditor is unable to perform the text conversion. So here's our getAsText() method:

public String getAsText() {
  // 1
  final Object value = this.getValue();
  // 2
  if (value == null) {
    return "";
  }
  // 3
  return value.toString();
}

And here are the details:

  1. Although we know we're always going to be working with java.net.URIs here, we technically speaking don't care what kind of value was installed, because ultimately (see 4) we're just going to return its String representation. So no cast needed here.
  2. This is the only gotcha in the contract for this method. The specification tells us indirectly that even when our installed value is null we must not return null from this method—unless we want to indicate that something broke in the conversion process, or that we simply don't support text conversion at all. Instead, we choose here to return the empty string, which is a good value to use in text fields and form input fields. And that's why we explicitly test for whitespace-only Strings in the setAsText() method, because of course the return value from the getAsText() method is often indirectly resupplied as the input to the setAsText method.
  3. Finally, we defer to the URI#toString() method for the "real" String representation because it just so happens to have really well-defined semantics. In more complicated cases, you may very well not want to defer to the toString() method. Specifically, you need to have a guarantee if you do defer to the toString() method that it will not return null, because although that may be OK for its contract, it's not OK for the PropertyEditor#getAsText() contract.

Lastly, we'll override the "value" methods just to make sure that they're nice and defensive and well-documented:

public Object getValue() {
  final Object uri = super.getValue();
  // 1
  assert uri == null || uri instanceof URI;
  return uri;
}

public void setValue(final Object value) {
  // 2
  if (value == null || value instanceof URI) {
    super.setValue(value);
  }
}
  1. The first thing we do is to make sure that no matter what happens later on in this class' lifecycle—no matter who "maintains" it after us, no matter what overridden methods in no matter what subclasses get butchered, we are intending this class to handle only java.net.URI instances and nothing else (well, except for Strings).
  2. In the setValue() method, we have a dilemma. The specification is silent about how a PropertyEditor is supposed to handle setValue() calls that it's not designed to handle. Once again, you can choose to throw a RuntimeException of your own choosing—the compiler certainly won't stop you and you're fully within your rights—but the typical caller of a PropertyEditor (usually some bit of infrastructure code) is not expecting you to call down fire and brimstone from this method. I tend to write my setValue() implementations to ignore invalid input. I obviously log the problem in such cases, but omitted the logging code here as installing logging here would be over the top for this example. Finally, recall that the instanceof operator will return false if null is its left-hand operand, so you have to explicitly check for that (in order to permit clearing out the installed value).

Callers

For reasons that I'll cover in the next installment, callers will expect to call this PropertyEditor in a way that looks something like this:

final PropertyEditor pe = // Get an appropriate property editor
final String text = // Gather user text
if (pe != null) {
  try {
    pe.setAsText(text);
  } catch (final IllegalArgumentException badText) {
    // Aha; the user input must have been bad.
    handleErrorVisuallyOrOtherwise(badText);
    return;
  }
  final Object canonicalValue = pe.getValue();
  final String canonicalText = pe.getAsText();
  if (canonicalText == null) {
    // Ah; this must mean that this particular PropertyEditor can't represent the value.
    // I'd better do something more fancy.
    maybeCreateSomeKindOfImage(canonicalValue);
  }
}

Observations and Summary

So, then, for Object-to-String conversion using PropertyEditors, keep the following things in mind:

  • Subclass PropertyEditorSupport.
  • Accept lousy input in your setAsText() method.
  • Never return null from your getAsText() method.
  • Define a non-null String you're going to use to represent the null value. Then make sure that your getAsText() method returns that to mean null, and that your setAsText() method is prepared to accept it to mean null.
  • Decide what to do when your setValue() method gets called with unexpected input.
  • Remember that you report invalid user text by throwing an IllegalArgumentException from your setAsText() method, and that's the only thing your callers are going to be prepared for.

If you build PropertyEditors bearing all these points in mind, then you will have a stable foundation for standardized Object-to-String conversion with well-defined semantics. We'll see in the next installment how to make all of Java's other Object-converting tools expressible in terms of PropertyEditors. That, in turn, will let you define all your conversion logic in one place.

In the next article, I'll cover java.text.Format, and will put togther a Format implementation that delegates to an underlying PropertyEditor. See you then.

Powered by ScribeFire.

Related Topics >>